Skip to main content
Glama
Licinexus

licinexus-mcp

Official
by Licinexus

compare_periodos

Compare two date ranges side-by-side over the same filters. Returns each period's total metrics plus absolute and percentage deltas.

Instructions

Compare two date ranges side-by-side over the same filters — answers questions like "did Jun/2024 (electoral year) differ from Jun/2025 in bid volumes?".

Wraps two aggregate_licitacoes_por_periodo calls and returns each period's total metrics plus absolute and percentage deltas. Use granularidade-style buckets implicitly = "ano" for the comparison (one bucket per period, summed).

When esfera filter or value metrics are requested, the underlying tool paginates internally — be conservative with range size.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
periodoAYes
periodoBYes
modalidadesNoModality codes. Default: [6, 8, 9].
ufNoTwo-letter state code.
codigoMunicipioIbgeNo
cnpjOrgaoNoProcuring agency CNPJ.
esferaNoFilter by sphere.
metricasNo

Implementation Reference

  • The main handler function for the compare_periodos tool. It parses args, calls aggregateLicitacoes.handler for both periods A and B in parallel, then computes absolute and percentage deltas between the two periods' metric totals.
      async handler(rawArgs) {
        const parse = ArgsSchema.safeParse(rawArgs ?? {});
        if (!parse.success) return errorResult(`Invalid arguments: ${parse.error.message}`);
        const args = parse.data;
    
        const baseArgs = {
          modalidades: args.modalidades,
          uf: args.uf,
          codigoMunicipioIbge: args.codigoMunicipioIbge,
          cnpjOrgao: args.cnpjOrgao,
          esfera: args.esfera,
          metricas: args.metricas,
          granularidade: 'ano' as const,
        };
    
        try {
          const [respA, respB] = await Promise.all([
            aggregateLicitacoes.handler({
              ...baseArgs,
              dataInicial: args.periodoA.dataInicial,
              dataFinal: args.periodoA.dataFinal,
            }),
            aggregateLicitacoes.handler({
              ...baseArgs,
              dataInicial: args.periodoB.dataInicial,
              dataFinal: args.periodoB.dataFinal,
            }),
          ]);
    
          if (respA.isError) return respA;
          if (respB.isError) return respB;
    
          const firstTextA = respA.content.find((c): c is { type: 'text'; text: string } => c.type === 'text');
          const firstTextB = respB.content.find((c): c is { type: 'text'; text: string } => c.type === 'text');
          if (!firstTextA || !firstTextB) {
            return errorResult('aggregate_licitacoes returned non-text content');
          }
          const dataA = JSON.parse(firstTextA.text) as AggregateResult;
          const dataB = JSON.parse(firstTextB.text) as AggregateResult;
    
          const totalsA = sumMetrics(dataA.series, args.metricas);
          const totalsB = sumMetrics(dataB.series, args.metricas);
    
          const delta: Record<string, { absoluto: number; percentual: number | null }> = {};
          for (const m of args.metricas) {
            const a = totalsA[m];
            const b = totalsB[m];
            const absoluto = b - a;
            const percentual = a === 0 ? null : (absoluto / a) * 100;
            delta[m] = { absoluto, percentual };
          }
    
          return jsonResult({
            meta: {
              modalidades: dataA.meta.modalidades,
              uf: args.uf,
              codigoMunicipioIbge: args.codigoMunicipioIbge,
              cnpjOrgao: args.cnpjOrgao,
              esfera: args.esfera,
              metricas: args.metricas,
            },
            periodoA: {
              label: args.periodoA.label,
              dataInicial: args.periodoA.dataInicial,
              dataFinal: args.periodoA.dataFinal,
              totals: totalsA,
            },
            periodoB: {
              label: args.periodoB.label,
              dataInicial: args.periodoB.dataInicial,
              dataFinal: args.periodoB.dataFinal,
              totals: totalsB,
            },
            delta,
          });
        } catch (err) {
          return errorResult(`Failed to compare periodos: ${err instanceof Error ? err.message : String(err)}`);
        }
      },
    };
  • Zod schema for the tool's input: periodoA, periodoB (each with label, dataInicial, dataFinal), optional modalidades, uf, codigoMunicipioIbge, cnpjOrgao, esfera, and metricas (count, valorEstimadoTotal, valorHomologadoTotal).
    const ArgsSchema = z.object({
      periodoA: PeriodoSchema,
      periodoB: PeriodoSchema,
      modalidades: z.array(z.number().int()).optional(),
      uf: z.string().length(2).toUpperCase().optional(),
      codigoMunicipioIbge: z.string().optional(),
      cnpjOrgao: z
        .string()
        .regex(/^\d{14}$/, 'CNPJ must be 14 digits, no punctuation')
        .optional(),
      esfera: EsferaSchema.optional(),
      metricas: z
        .array(z.enum(['count', 'valorEstimadoTotal', 'valorHomologadoTotal']))
        .default(['count']),
    });
  • Import and registration of comparePeriodos in the allTools array, making it available in the toolMap.
    import { comparePeriodos } from './compare_periodos.js';
    
    export const allTools: ToolDef[] = [
      // Compras / Licitações
      searchLicitacoes,
      getLicitacao,
      listLicitacaoItens,
      listLicitacaoResultados,
      listLicitacaoArquivos,
      // Contratos
      searchContratos,
      getContratoTool,
      listContratoTermosTool,
      listContratoInstrumentosTool,
      // Atas RP
      searchAtasRp,
      getAtaRp,
      // Órgãos / Fornecedores
      getOrgaoTool,
      getFornecedorContratos,
      // PCA
      searchPca,
      listPcaItensTool,
      // CNPJ enrichment
      getCnpjDataTool,
      // Análise agregada (v0.2.0)
      aggregateLicitacoes,
      comparePeriodos,
    ];
  • Tool definition with name 'compare_periodos', description, and JSON Schema input schema (properties for periodoA, periodoB, modalidades, uf, codigoMunicipioIbge, cnpjOrgao, esfera, metricas).
    export const comparePeriodos: ToolDef = {
      definition: {
        name: 'compare_periodos',
        description: [
          'Compare two date ranges side-by-side over the same filters — answers questions like "did Jun/2024 (electoral year) differ from Jun/2025 in bid volumes?".',
          '',
          'Wraps two `aggregate_licitacoes_por_periodo` calls and returns each period\'s total metrics plus absolute and percentage deltas. Use granularidade-style buckets implicitly = "ano" for the comparison (one bucket per period, summed).',
          '',
          'When `esfera` filter or value metrics are requested, the underlying tool paginates internally — be conservative with range size.',
        ].join('\n'),
        inputSchema: {
          type: 'object',
          required: ['periodoA', 'periodoB'],
          properties: {
            periodoA: {
              type: 'object',
              required: ['label', 'dataInicial', 'dataFinal'],
              properties: {
                label: { type: 'string', description: 'Friendly label, e.g. "Jun/2024"' },
                dataInicial: { type: 'string', description: 'YYYYMMDD' },
                dataFinal: { type: 'string', description: 'YYYYMMDD' },
              },
            },
            periodoB: {
              type: 'object',
              required: ['label', 'dataInicial', 'dataFinal'],
              properties: {
                label: { type: 'string', description: 'Friendly label, e.g. "Jun/2025"' },
                dataInicial: { type: 'string', description: 'YYYYMMDD' },
                dataFinal: { type: 'string', description: 'YYYYMMDD' },
              },
            },
            modalidades: {
              type: 'array',
              items: { type: 'integer', enum: MODALIDADE_IDS },
              description: 'Modality codes. Default: [6, 8, 9].',
            },
            uf: { type: 'string', description: 'Two-letter state code.' },
            codigoMunicipioIbge: { type: 'string' },
            cnpjOrgao: { type: 'string', description: 'Procuring agency CNPJ.' },
            esfera: {
              type: 'string',
              enum: [...ESFERA_VALUES],
              description: "Filter by sphere.",
            },
            metricas: {
              type: 'array',
              items: { type: 'string', enum: ['count', 'valorEstimadoTotal', 'valorHomologadoTotal'] },
              default: ['count'],
            },
          },
        },
      },
  • Helper function sumMetrics that sums numeric metric values across a series array (used to aggregate totals from the aggregate_licitacoes results).
    function sumMetrics(
      series: Array<Record<string, number | string>>,
      metricas: string[],
    ): Record<string, number> {
      const sums: Record<string, number> = {};
      for (const m of metricas) sums[m] = 0;
      for (const row of series) {
        for (const m of metricas) {
          const v = row[m];
          if (typeof v === 'number') sums[m] += v;
        }
      }
      return sums;
    }
Behavior3/5

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

No annotations provided, so the description is the sole source. It discloses internal pagination when specific filters are applied and advises caution with range size. However, it does not mention read-only nature, authentication, or other side effects.

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?

Description is three sentences, front-loaded with a clear example and use case. Concise but could be slightly tighter; still efficient.

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?

No output schema exists, so the description must explain return values. It states the output includes 'total metrics plus absolute and percentage deltas' but lacks precise structure. The pagination hint adds context, but overall completeness is adequate but not thorough.

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

Parameters2/5

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

Schema description coverage is 50%, with some parameters having descriptions in the schema. The description adds little beyond stating implicit bucket granularity and an example label, but does not clarify undocumented parameters like `codigoMunicipioIbge` or `cnpjOrgao`.

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 tool compares two date ranges with the same filters, using a concrete example question. It distinguishes from its sibling `aggregate_licitacoes_por_periodo` by explaining it wraps two calls.

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 comparison but does not explicitly state when to use this tool versus `aggregate_licitacoes_por_periodo` alone. It mentions internal pagination but no guidance on when not to use or prerequisites.

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/Licinexus/licinexus-mcp'

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