Skip to main content
Glama

AroFlo: Get PurchaseOrders

aroflo_get_purchaseorders
Read-onlyIdempotent

Retrieve and filter purchase order data from AroFlo using customizable queries, joins, and output modes to manage procurement workflows.

Instructions

Query the AroFlo PurchaseOrders zone (GET). Use pipe-delimited WHERE clauses like "and|field|=|value", ORDER clauses like "field|asc", and JOIN areas like "lineitems". where/order/join accept either a single string or an array. mode: data|verbose|debug|raw (default: data). Set compact=true and optionally select=["field","nested.field"] to reduce payload size. See resource "aroflo://docs/api/" (example: "aroflo://docs/api/quotes") for valid fields/values.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
whereNo
orderNo
joinNo
pageNo
pageSizeNo
autoPaginateNo
maxPagesNo
maxResultsNo
maxItemsTotalNo
validateWhereNo
modeNo
verboseNo
debugNo
rawNo
compactNo
selectNo
maxItemsNo
extraNo

Output Schema

TableJSON Schema
NameRequiredDescriptionDefault

No arguments

Implementation Reference

  • Registration and handler implementation for 'aroflo_report_open_projects_with_task_hours' (a similar reporting tool). Although 'aroflo_get_purchaseorders' was not found in the codebase, the structure for registering such tools in 'src/mcp/app.ts' and 'src/mcp/tools/*.ts' has been confirmed.
      server.registerTool(
        'aroflo_report_open_projects_with_task_hours',
        {
          title: 'AroFlo: Report Open Projects With Task Hours',
          description:
            'Report open projects (status=open, closeddate empty) and their tasks with total labour hours. ' +
            'Internally fetches Projects then Tasks (join project + tasktotals) and returns a compact grouped output.',
          inputSchema: {
            sinceCreatedUtc: z.string().min(1).optional(),
            orgId: z.string().min(1).optional(),
            managerUserId: z.string().min(1).optional(),
            sinceDateRequested: z.string().min(1).optional(),
            sinceTaskCreatedUtc: z.string().min(1).optional(),
            includeTasksWithoutProject: z.boolean().default(false),
            hoursOnly: z.boolean().default(false),
            includeTaskStatus: z.boolean().default(true),
            pageSize: z.number().int().positive().max(500).default(200),
            maxProjects: z.number().int().positive().max(5000).default(2000),
            maxTasksPerClient: z.number().int().positive().max(5000).default(2000),
            mode: z.enum(['data', 'verbose', 'debug', 'raw']).optional(),
            verbose: z.boolean().optional(),
            debug: z.boolean().optional()
          },
          // MCP SDK expects output schemas to be object schemas (or raw object shapes).
          // `z.any()` causes output validation to crash under the current SDK.
          outputSchema: z.object({}).passthrough(),
          annotations: {
            readOnlyHint: true,
            idempotentHint: true,
            openWorldHint: true
          }
        },
        async (args) => {
          const mode = resolveOutputMode(args);
          try {
            const whereClauses: string[] = [];
            if (args.orgId) {
              whereClauses.push(`and|orgid|=|${args.orgId}`);
            }
            if (args.sinceCreatedUtc) {
              whereClauses.push(`and|createdutc|>|${args.sinceCreatedUtc}`);
            }
            const where = normalizeWhereParam(whereClauses);
    
            const pageSize = args.pageSize;
            const maxProjects = args.maxProjects;
    
            let projectsResponse = await client.get('Projects', { where, page: 1, pageSize });
            let pagesFetchedProjects = 1;
            while (true) {
              const projects = getProjectsArray(projectsResponse.data);
              if (projects.length >= maxProjects) {
                projectsResponse = {
                  ...projectsResponse,
                  data: truncateZoneArrays(projectsResponse.data, maxProjects).truncated
                };
                break;
              }
              if (projects.length < pageSize) {
                break;
              }
              const nextPage = pagesFetchedProjects + 1;
              const next = await client.get('Projects', { where, page: nextPage, pageSize });
              const nextProjects = getProjectsArray(next.data);
              if (nextProjects.length === 0) {
                break;
              }
              projectsResponse = {
                ...projectsResponse,
                data: mergeZoneResponseData(projectsResponse.data, next.data).merged
              };
              pagesFetchedProjects += 1;
              if (nextProjects.length < pageSize) {
                break;
              }
            }
    
            const allProjects = getProjectsArray(projectsResponse.data);
            const openProjects = pickOpenProjects(allProjects, args.managerUserId);
    
            const openProjectIds = new Set<string>();
            const clientIds = new Set<string>();
    
            for (const p of openProjects) {
              if (!isRecord(p)) continue;
              const pid = typeof p.projectid === 'string' ? p.projectid : '';
              if (pid) openProjectIds.add(pid);
              const c = p.client;
              const cid = isRecord(c) && typeof c.orgid === 'string' ? c.orgid : '';
              if (cid) clientIds.add(cid);
            }
    
            const tasksByProject: Record<string, any[]> = {};
            for (const pid of openProjectIds) {
              tasksByProject[pid] = [];
            }
            const unassignedByClient: Record<string, any[]> = {};
    
            const perClientMeta: Record<string, unknown> = {};
    
            for (const clientId of clientIds) {
              const taskWhere: string[] = [`and|clientid|=|${clientId}`];
              if (args.sinceDateRequested) {
                taskWhere.push(`and|daterequested|>|${args.sinceDateRequested}`);
              }
              if (args.sinceTaskCreatedUtc) {
                taskWhere.push(`and|createdutc|>|${args.sinceTaskCreatedUtc}`);
              }
    
              const normalizedTaskWhere = normalizeWhereParam(taskWhere);
    
              let tasksResponse = await client.get('Tasks', {
                where: normalizedTaskWhere,
                join: ['project', 'tasktotals'],
                order: ['daterequested|desc'],
                page: 1,
                pageSize
              });
    
              let pagesFetchedTasks = 1;
              while (true) {
                const tasks = getTasksArray(tasksResponse.data);
                if (tasks.length >= args.maxTasksPerClient) {
                  tasksResponse = {
                    ...tasksResponse,
                    data: truncateZoneArrays(tasksResponse.data, args.maxTasksPerClient).truncated
                  };
                  break;
                }
                if (tasks.length < pageSize) {
                  break;
                }
                const nextPage = pagesFetchedTasks + 1;
                const next = await client.get('Tasks', {
                  where: normalizedTaskWhere,
                  join: ['project', 'tasktotals'],
                  order: ['daterequested|desc'],
                  page: nextPage,
                  pageSize
                });
                const nextTasks = getTasksArray(next.data);
                if (nextTasks.length === 0) {
                  break;
                }
                tasksResponse = {
                  ...tasksResponse,
                  data: mergeZoneResponseData(tasksResponse.data, next.data).merged
                };
                pagesFetchedTasks += 1;
                if (nextTasks.length < pageSize) {
                  break;
                }
              }
    
              const tasks = getTasksArray(tasksResponse.data).filter(isRecord);
              let matched = 0;
              let unassigned = 0;
    
              for (const t of tasks) {
                const proj = t.project;
                const joinedProjectId =
                  isRecord(proj) && typeof proj.projectid === 'string' ? proj.projectid : undefined;
                const rawProjectId = typeof t.projectid === 'string' ? t.projectid : undefined;
                const projectId = joinedProjectId ?? rawProjectId;
    
                const totals = t.tasktotals;
                const hours = isRecord(totals) ? toNumber(totals.totalhrs) : 0;
                if (args.hoursOnly && hours <= 0) {
                  continue;
                }
    
                const dateRequested =
                  (t.daterequested as any) ??
                  (t.requestdate as any) ??
                  (t.requestdatetime as any) ??
                  undefined;
    
                const mapped: Record<string, unknown> = {
                  taskid: t.taskid,
                  refcode: t.refcode,
                  taskname: t.taskname,
                  daterequested: dateRequested,
                  hours
                };
                if (args.includeTaskStatus) {
                  mapped.status = t.status;
                }
    
                if (projectId && openProjectIds.has(projectId)) {
                  tasksByProject[projectId]!.push(mapped);
                  matched += 1;
                  continue;
                }
    
                if (args.includeTasksWithoutProject) {
                  if (!unassignedByClient[clientId]) unassignedByClient[clientId] = [];
                  unassignedByClient[clientId]!.push(mapped);
                  unassigned += 1;
                }
              }
    
              perClientMeta[clientId] = {
                pagesFetched: pagesFetchedTasks,
                totalTasksFetched: tasks.length,
                matched,
                unassigned
              };
            }
    
            const projects = openProjects
              .filter(isRecord)
              .map((p) => {
                const pid = p.projectid as string;
                const tasks = (tasksByProject[pid] ?? []).filter(isRecord).sort((a, b) => {
                  const ad = String((a as any).daterequested ?? '');
                  const bd = String((b as any).daterequested ?? '');
                  if (ad !== bd) return ad.localeCompare(bd);
                  return String((a as any).refcode ?? '').localeCompare(
                    String((b as any).refcode ?? '')
                  );
                });
    
                const totalHours = tasks.reduce((sum, t) => sum + toNumber((t as any).hours), 0);
                const projectname = typeof p.projectname === 'string' ? p.projectname : undefined;
                const projectnumber = p.projectnumber;
                const clientOrg = isRecord(p.client) ? p.client : undefined;
    
                return {
                  projectid: pid,
                  projectnumber,
                  projectname,
                  refno: p.refno,
                  client: clientOrg
                    ? { orgid: clientOrg.orgid, orgname: clientOrg.orgname }
                    : undefined,
                  totalHours,
                  tasks
                };
              })
              .sort((a, b) => (toInt(a.projectnumber) ?? 0) - (toInt(b.projectnumber) ?? 0));
    
            const projectCount = projects.length;
            const taskCount = projects.reduce((sum, p) => sum + (p.tasks as any[]).length, 0);
            const totalHours = projects.reduce((sum, p) => sum + toNumber(p.totalHours), 0);
    
            const out: Record<string, unknown> = {
              projects,
              summary: { projectCount, taskCount, totalHours }
            };
    
            if (args.includeTasksWithoutProject) {
              out.unassignedTasksByClient = unassignedByClient;
            }
    
            if (mode === 'verbose' || mode === 'debug' || mode === 'raw') {
              out.meta = {
                projectsFetched: allProjects.length,
                openProjects: openProjects.length,
                pagesFetchedProjects,
                clientsQueried: Array.from(clientIds),
                perClient: perClientMeta
              };
            }
    
            if (mode === 'debug' || mode === 'raw') {
              out.debug = args.debug ? { whereProjects: where } : undefined;
            }
    
            if (mode === 'raw') {
              out.raw = { openProjects };
            }
    
            return successToolResult(out);
          } catch (error) {
            return errorToolResult(error, {
              mode,
              debug: { tool: 'aroflo_report_open_projects_with_task_hours' }
            });
          }
        }
      );
    }
Behavior4/5

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

Beyond annotations (readOnly/idempotent), the description adds valuable behavioral context: the pipe-delimited syntax format for WHERE/ORDER clauses ('and|field|=|value'), the mode enum options (data|verbose|debug|raw), payload optimization via compact/select, and references to external documentation resources for valid fields.

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

Conciseness5/5

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

Information-dense structure with zero waste: leads with purpose, follows immediately with syntax examples and format specifications, then optimization hints, ending with documentation reference. Every clause provides actionable formatting guidance.

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

Completeness4/5

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

Given the 18-parameter complexity and lack of schema descriptions, the description adequately covers the primary query interface. However, it should mention pagination limits (max 500 per page visible in schema) or explain the duplicate control flags (verbose/debug/raw) that mirror the mode parameter.

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

Parameters4/5

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

With 0% schema description coverage, the description compensates well by detailing the complex query parameters (where/order/join) with syntax examples and type flexibility (string or array), plus explaining mode and select. However, it omits semantics for pagination controls (autoPaginate, maxPages, pageSize) and the relationship between mode and the verbose/debug/raw booleans.

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 opens with specific verb 'Query', identifies the exact resource 'AroFlo PurchaseOrders zone', and includes the HTTP method '(GET)' to distinguish it from sibling tools like aroflo_create_or_update_record and other zone-specific getters.

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?

While the specific zone 'PurchaseOrders' is clear from the name and description, there is no explicit guidance on when to use this versus the generic aroflo_query_zone or aroflo_get_record. Usage is implied by the naming convention but not stated.

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/0x1NotMe/aroflo-mcp'

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