All files / src/mcp/imf observations.ts

93.54% Statements 29/31
86.66% Branches 26/30
100% Functions 4/4
100% Lines 25/25

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79                                          35x 24x 6x 2x 3x   1x                 24x 24x                         5x 5x 2x 2x 2x   1x       4x 5x   4x 3x 3x 1x   3x 2x 4x 4x       3x      
// SPDX-FileCopyrightText: 2024-2026 Hack23 AB
// SPDX-License-Identifier: Apache-2.0
 
/**
 * @module MCP/imf/observations
 * @description SDMX observation counting and MCP result-wrapping helpers.
 */
 
import type { MCPToolResult } from '../../types/index.js';
import type { SDMXObservationPayload } from './types.js';
 
/**
 * Unwrap SDMX localised labels to a plain string.
 *
 * SDMX 3.0 sometimes returns `name`/`description` as a language-keyed
 * object (`{ en: "World Economic Outlook" }`); older payloads return a
 * raw string. Prefer English, fall back to the first available value.
 * @param raw - Raw label value from SDMX API response.
 * @returns Plain string, or empty string when `raw` is empty/undefined.
 */
export function unwrapLocalisedLabel(raw: string | Record<string, string> | undefined): string {
  if (!raw) return '';
  if (typeof raw === 'string') return raw;
  if (typeof raw['en'] === 'string') return raw['en'];
  for (const v of Object.values(raw)) {
    if (typeof v === 'string') return v;
  }
  return '';
}
 
/**
 * Wrap a JSON-serialisable value in the canonical MCP tool-result shape.
 * @param payload - Any JSON-serialisable value to embed.
 * @returns MCP tool result with `content[0].type === 'text'`.
 */
export function wrapAsMCPResult(payload: unknown): MCPToolResult {
  const text = typeof payload === 'string' ? payload : JSON.stringify(payload ?? null);
  return { content: [{ type: 'text', text }] };
}
 
/**
 * Count observations in an IMF SDMX-JSON data payload.
 *
 * The IMF API can return observations either directly on a dataset or nested
 * under `data.dataSets[].series[*].observations`.
 *
 * @param payload - Raw JSON string or already-parsed SDMX-JSON payload.
 * @returns Number of observation cells found; `0` for invalid or empty input.
 */
export function countIMFSDMXObservations(payload: string | unknown): number {
  let parsed: unknown = payload;
  if (typeof payload === 'string') {
    Iif (!payload.trim()) return 0;
    try {
      parsed = JSON.parse(payload);
    } catch {
      return 0;
    }
  }
 
  const dataSets = (parsed as SDMXObservationPayload | null)?.data?.dataSets;
  Iif (!Array.isArray(dataSets)) return 0;
 
  return dataSets.reduce((total, dataSet) => {
    let count = 0;
    if (dataSet.observations && typeof dataSet.observations === 'object') {
      count += Object.keys(dataSet.observations).length;
    }
    if (dataSet.series && typeof dataSet.series === 'object') {
      for (const row of Object.values(dataSet.series)) {
        Eif (row?.observations && typeof row.observations === 'object') {
          count += Object.keys(row.observations).length;
        }
      }
    }
    return total + count;
  }, 0);
}