/** * @fileoverview World Bank Projects API service. Wraps the `/projects` search * endpoint, which shares neither a host, an envelope, a pagination model, nor an * error convention with the Indicators v2 API or with PIP: results arrive as an * object keyed by project ID, paging is offset-based, and an exact-match filter * that matches nothing answers HTTP 200 with `total: 0` rather than an error. * That last one is the reason this service probes the country filter in * isolation whenever a search comes back empty — a zero hit that reads as "no * results" is otherwise indistinguishable from a country code the portfolio has * never heard of. * @module services/projects/projects-service */ import type { Context } from '@cyanheads/mcp-ts-core'; import type { AppConfig } from '@cyanheads/mcp-ts-core/config'; import type { StorageService } from '@cyanheads/mcp-ts-core/storage'; import type { ProjectSummary } from './types.js'; export type ProjectSearchOptions = { approvedFrom?: string; approvedTo?: string; countryCodes: string[]; /** Financing windows (`IBRD`, `IDA`, `Grants`, `Other`), combined as OR. */ financialTypes: string[]; includeAbstract: boolean; page: number; perPage: number; query?: string; regions: string[]; statuses: string[]; }; export type ProjectSearchResult = { /** * Projects matching the country filter on its own, measured only when the * search itself returned nothing and a country filter was in force. Zero means * no project in the portfolio carries any of those codes; a positive number * means the codes match as a set — the OR of them, not each individually — and * the other filters are what emptied the result. Null when no probe ran, or * when it ran and failed. */ countryOnlyTotal: number | null; page: number; pages: number; /** Page size actually served: the requested size, reduced to the page cap when larger. */ perPage: number; projects: ProjectSummary[]; total: number; }; export declare class ProjectsService { private readonly baseUrl; constructor(_config: AppConfig, _storage: StorageService); /** * Every request is sorted by board approval date, newest first, so the order * is this server's choice rather than an upstream default. It has to be: * without `qterm` that is already what upstream returns, but with `qterm` its * own order is descending project ID, which leaves the newest approvals pages * deep. The sort lives here, not in the filters, so no filter can drop it. */ private buildUrl; /** Fetch one `/projects` response and normalize its envelope into rows plus a count. */ private fetchPage; /** * Search the World Bank lending portfolio. * * Every filter this accepts is an exact match upstream, and an exact match on a * value the index does not hold returns HTTP 200 with `total: 0` — the same * response a real no-match produces. Enum-backed filters cannot reach that * state because the schema rejects an unknown value before the request, but a * country code cannot be checked that way: it is well-formed at two characters * and only the index knows whether the portfolio has ever used it. So an empty * result with a country filter in force costs one extra `rows=0` request that * asks the country filter alone, which separates "that code matches nothing at * all" from "the codes are fine, the combination is what is empty". The probe * asks the codes as one OR-set, so a positive count settles the set rather * than each code, and a probe that fails leaves the empty result untouched. */ searchProjects(opts: ProjectSearchOptions, ctx: Context): Promise; } export declare function initProjectsService(config: AppConfig, storage: StorageService): void; export declare function getProjectsService(): ProjectsService; //# sourceMappingURL=projects-service.d.ts.map