All files / src/tools getExternalDocumentsFeed.ts

95% Statements 38/40
83.33% Branches 20/24
100% Functions 6/6
96.87% Lines 31/32

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 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168                                        4x                                           2x     2x 2x   2x                     11x 11x       11x 1x     11x 1x             1x 1x                                           15x 15x   1x 1x 1x                     14x 14x 14x 14x 14x 14x     12x 12x 1x       11x           2x 1x                   4x                                          
/**
 * MCP Tool: get_external_documents_feed
 *
 * Get recently updated external documents (non-EP documents) from the feed.
 *
 * **EP API Endpoint:**
 * - `GET /external-documents/feed`
 *
 * ISMS Policy: SC-002 (Input Validation), AC-003 (Least Privilege)
 */
 
import { GetExternalDocumentsFeedSchema } from '../schemas/europeanParliament.js';
import { epClient } from '../clients/europeanParliamentClient.js';
import { ToolError } from './shared/errors.js';
import { isUpstream404, isErrorInBody, buildFeedSuccessResponse, buildEmptyFeedResponse } from './shared/feedUtils.js';
import { buildToolResponse } from './shared/responseBuilder.js';
import { z } from 'zod';
import type { ToolResult } from './shared/types.js';
 
const EXTERNAL_DOCUMENTS_EMPTY_REASON =
  'EP external-documents feed returned zero items for the requested window; this is ambiguous between a true empty window and feed freshness/ordering lag.';
 
interface ExternalDocumentsFeedDiagnostics {
  endpoint: 'external-documents/feed';
  requestedWindow: {
    timeframe: string;
    startDate?: string;
    workType?: string;
  };
  emptyResultAmbiguity: 'true-empty-or-feed-freshness-lag';
  freshnessStatus: 'unknown';
  fallbackTool: 'get_external_documents';
  fallbackArguments: {
    limit: 20;
  };
}
 
type ExternalDocumentsFeedParams = ReturnType<typeof GetExternalDocumentsFeedSchema.parse>;
 
function buildExternalDocumentsFeedDiagnostics(
  params: ExternalDocumentsFeedParams
): ExternalDocumentsFeedDiagnostics {
  const requestedWindow: ExternalDocumentsFeedDiagnostics['requestedWindow'] = {
    timeframe: params.timeframe,
  };
  if (params.startDate !== undefined) requestedWindow.startDate = params.startDate;
  if (params.workType !== undefined) requestedWindow.workType = params.workType;
 
  return {
    endpoint: 'external-documents/feed',
    requestedWindow,
    emptyResultAmbiguity: 'true-empty-or-feed-freshness-lag',
    freshnessStatus: 'unknown',
    fallbackTool: 'get_external_documents',
    fallbackArguments: { limit: 20 },
  };
}
 
function hasFeedItems(result: unknown): boolean {
  const source = (result ?? {}) as Record<string, unknown>;
  return Array.isArray(source['data']) && source['data'].length > 0;
}
 
function withEmptyFeedDiagnostics(result: unknown, params: ExternalDocumentsFeedParams): unknown {
  if (hasFeedItems(result)) return result;
  const source = (result ?? {}) as Record<string, unknown>;
  // Error-in-body responses are upstream enrichment failures, not ambiguous
  // true-empty/freshness-lag windows — do not attach diagnostics.
  Iif (isErrorInBody(source)) return result;
  return {
    ...source,
    dataQualityDiagnostics: buildExternalDocumentsFeedDiagnostics(params),
  };
}
 
function buildExternalDocumentsUnavailableResponse(params: ExternalDocumentsFeedParams): ToolResult {
  const items: unknown[] = [];
  return buildToolResponse({
    status: 'unavailable',
    generatedAt: new Date().toISOString(),
    items,
    itemCount: 0,
    reason: EXTERNAL_DOCUMENTS_EMPTY_REASON,
    data: items,
    '@context': [],
    dataQualityWarnings: [EXTERNAL_DOCUMENTS_EMPTY_REASON],
    dataQualityDiagnostics: buildExternalDocumentsFeedDiagnostics(params),
  });
}
 
/**
 * Handles the get_external_documents_feed MCP tool request.
 *
 * @param args - Raw tool arguments, validated against {@link GetExternalDocumentsFeedSchema}
 * @returns MCP tool result containing recently updated external document data
 * @security Input is validated with Zod before any API call.
 */
export async function handleGetExternalDocumentsFeed(args: unknown): Promise<ToolResult> {
  let params: ReturnType<typeof GetExternalDocumentsFeedSchema.parse>;
  try {
    params = GetExternalDocumentsFeedSchema.parse(args);
  } catch (error: unknown) {
    Eif (error instanceof z.ZodError) {
      const fieldErrors = error.issues.map((e) => `${e.path.join('.')}: ${e.message}`).join('; ');
      throw new ToolError({
        toolName: 'get_external_documents_feed',
        operation: 'validateInput',
        message: `Invalid parameters: ${fieldErrors}`,
        isRetryable: false,
        cause: error,
      });
    }
    throw error;
  }
 
  try {
    const apiParams: Record<string, unknown> = {};
    apiParams['timeframe'] = params.timeframe;
    if (params.startDate !== undefined) apiParams['startDate'] = params.startDate;
    if (params.workType !== undefined) apiParams['workType'] = params.workType;
    const result = await epClient.getExternalDocumentsFeed(
      apiParams
    );
    const source = result as Record<string, unknown>;
    if (isErrorInBody(source)) {
      return buildEmptyFeedResponse(
        'EP API returned an error-in-body response for get_external_documents_feed — the upstream enrichment step may have failed.',
      );
    }
    return buildFeedSuccessResponse(
      withEmptyFeedDiagnostics(result, params),
      [],
      EXTERNAL_DOCUMENTS_EMPTY_REASON,
    );
  } catch (error: unknown) {
    if (isUpstream404(error)) return buildExternalDocumentsUnavailableResponse(params);
    throw new ToolError({
      toolName: 'get_external_documents_feed',
      operation: 'fetchData',
      message: 'Failed to retrieve external documents feed',
      isRetryable: true,
      cause: error,
    });
  }
}
/** Tool metadata for get_external_documents_feed */
export const getExternalDocumentsFeedToolMetadata = {
  name: 'get_external_documents_feed',
  description:
    'Get recently updated external documents (non-EP documents) from the feed. Returns external documents published or updated during the specified timeframe. Data source: European Parliament Open Data Portal.',
  inputSchema: {
    type: 'object' as const,
    properties: {
      timeframe: {
        type: 'string',
        description: 'Timeframe for the feed (today, one-day, one-week, one-month, custom)',
        enum: ['today', 'one-day', 'one-week', 'one-month', 'custom'],
        default: 'one-week',
      },
      startDate: {
        type: 'string',
        description: 'Start date (YYYY-MM-DD) — required when timeframe is "custom"',
      },
      workType: { type: 'string', description: 'Work type filter' },
    },
  },
};