Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/gpf/adminexpress.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ const ADMINEXPRESS_TYPENAMES = ADMINEXPRESS_TYPES.map((type) => `ADMINEXPRESS-CO
* @returns Administrative units covering the requested point.
*/
export async function getAdminUnits(lon: number, lat: number): Promise<AdminUnit[]> {
logger.info(`[adminexpress] getAdminUnits(${lon},${lat})...`);
logger.debug(`[gpf:adminexpress] getAdminUnits(${lon},${lat})...`);

const spatialFilter: SpatialFilter = { operator: "intersects_point", lon, lat };

Expand Down
2 changes: 1 addition & 1 deletion src/gpf/altitude.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ type AltitudeResult = {
* @returns {Promise<AltitudeResult>}
*/
export async function getAltitudeByLocation(lon: number, lat: number, fetcher: JsonFetcher<RawAltitudeResponse> = fetchJSONGet): Promise<AltitudeResult> {
logger.info(`getAltitudeByLocation(${lon},${lat})...`);
logger.debug(`[gpf:altitude] getAltitudeByLocation(${lon},${lat})...`);

const url = `https://data.geopf.fr/altimetrie/1.0/calcul/alti/rest/elevation.json?lon=${lon}&lat=${lat}&resource=ign_rge_alti_wld`;

Expand Down
2 changes: 1 addition & 1 deletion src/gpf/geocode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export async function geocode(text: string, maximumResponses = 3, fetcher: JsonF
return [];
}

logger.info(`geocode(${JSON.stringify(normalizedText)}, ${maximumResponses})...`);
logger.debug(`[gpf:geocode] geocode(${JSON.stringify(normalizedText)}, ${maximumResponses})...`);

const url = 'https://data.geopf.fr/geocodage/completion/?' + new URLSearchParams({
text: normalizedText,
Expand Down
48 changes: 24 additions & 24 deletions src/gpf/parcellaire-express.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,30 +69,30 @@ function filterByDistance(items: ParcellaireExpressItem[]): ParcellaireExpressIt
* @param lat Latitude of the query point.
* @returns The nearest cadastral objects, at most one per cadastral type.
*/
export async function getParcellaireExpress(lon: number, lat: number): Promise<ParcellaireExpressItem[]> {
logger.info(`getParcellaireExpress(${lon},${lat}) ...`);

const spatialFilter: SpatialFilter = {
operator: "dwithin_point",
lon,
lat,
distance_m: 10,
};

// Resolve and compile one spatial filter per typename to avoid relying on
// cross-layer geometry property homogeneity.
const cqlFilters = await Promise.all(PARCELLAIRE_EXPRESS_TYPENAMES.map(async (typename) => {
const featureType = await getFeatureType(typename);
const geometryProperty = getGeometryProperty(featureType);
return compileDwithinSpatialFilter(geometryProperty, spatialFilter);
}));

// Execute the multi-typename WFS query
const featureCollection: WfsFeatureCollectionResponse = await fetchWfsMultiTypename({
typenames: PARCELLAIRE_EXPRESS_TYPENAMES,
cqlFilters,
errorLabel: 'PARCELLAIRE_EXPRESS',
});
export async function getParcellaireExpress(lon: number, lat: number): Promise<ParcellaireExpressItem[]> {
logger.debug(`[gpf:parcellaire-express] getParcellaireExpress(${lon},${lat}) ...`);
const spatialFilter: SpatialFilter = {
operator: "dwithin_point",
lon,
lat,
distance_m: 10,
};
// Resolve and compile one spatial filter per typename to avoid relying on
// cross-layer geometry property homogeneity.
const cqlFilters = await Promise.all(PARCELLAIRE_EXPRESS_TYPENAMES.map(async (typename) => {
const featureType = await getFeatureType(typename);
const geometryProperty = getGeometryProperty(featureType);
return compileDwithinSpatialFilter(geometryProperty, spatialFilter);
}));
// Execute the multi-typename WFS query
const featureCollection: WfsFeatureCollectionResponse = await fetchWfsMultiTypename({
typenames: PARCELLAIRE_EXPRESS_TYPENAMES,
cqlFilters,
errorLabel: 'PARCELLAIRE_EXPRESS',
});

// Map to flat items preserving geometry for distance calculation
const sourceGeom: Point = { type: "Point", coordinates: [lon, lat] };
Expand Down
4 changes: 2 additions & 2 deletions src/gpf/urbanisme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ function sanitizeUrbanismeItem(item: UrbanismeItem): Record<string, unknown> {
* @returns Urban planning objects relevant to the requested point.
*/
export async function getUrbanisme(lon: number, lat: number): Promise<Record<string, unknown>[]> {
logger.info(`getUrbanisme(${lon},${lat})...`);
logger.debug(`[gpf:urbanisme] getUrbanisme(${lon},${lat})...`);

const spatialFilter: SpatialFilter = {
operator: "dwithin_point",
Expand Down Expand Up @@ -115,7 +115,7 @@ const ASSIETTES_SUP_TYPES = [
* @returns SUP footprints relevant to the requested point.
*/
export async function getAssiettesServitudes(lon: number, lat: number): Promise<UrbanismeItem[]> {
logger.info(`getAssiettesServitudes(${lon},${lat})...`);
logger.debug(`[gpf:urbanisme] getAssiettesServitudes(${lon},${lat})...`);

const spatialFilter: SpatialFilter = {
operator: "dwithin_point",
Expand Down
2 changes: 1 addition & 1 deletion src/helpers/wfs_engine/features.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ export async function executeGetFeatures(input: GpfWfsGetFeaturesInput) {
let featureCollection: WfsFeatureCollectionResponse;

try {
logger.info(
logger.debug(
`[gpf_wfs_get_features] POST ${request.url}?${new URLSearchParams(request.query).toString()}`,
);
featureCollection = await fetchFeatureCollection(request);
Expand Down
5 changes: 5 additions & 0 deletions src/tools/AdminexpressTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { z } from "zod";
import { getAdminUnits, ADMINEXPRESS_TYPES, ADMINEXPRESS_SOURCE } from "../gpf/adminexpress.js";
import { READ_ONLY_OPEN_WORLD_TOOL_ANNOTATIONS } from "../helpers/toolAnnotations.js";
import { featureRefSchema, lonSchema, latSchema } from "../helpers/schemas.js";
import logger from "../logger.js";

// --- Schema ---

Expand Down Expand Up @@ -57,6 +58,10 @@ class AdminexpressTool extends BaseTool<AdminexpressInput> {
* @returns The matching administrative units enriched with reusable `feature_ref` metadata.
*/
async execute(input: AdminexpressInput) {
logger.info(`[tool] execute ${this.name} ...`, {
input: input
});

return {
results: await getAdminUnits(input.lon, input.lat),
};
Expand Down
5 changes: 5 additions & 0 deletions src/tools/AltitudeTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { z } from "zod";
import { ALTITUDE_SOURCE, getAltitudeByLocation } from "../gpf/altitude.js";
import { READ_ONLY_OPEN_WORLD_TOOL_ANNOTATIONS } from "../helpers/toolAnnotations.js";
import { lonSchema, latSchema } from "../helpers/schemas.js";
import logger from "../logger.js";

// --- Schema ---

Expand Down Expand Up @@ -47,6 +48,10 @@ class AltitudeTool extends BaseTool<AltitudeInput> {
* @returns The altitude payload returned by the upstream service.
*/
async execute(input: AltitudeInput) {
logger.info(`[tool] execute ${this.name} ...`, {
input: input
});

return await getAltitudeByLocation(input.lon, input.lat);
}
}
Expand Down
5 changes: 5 additions & 0 deletions src/tools/AssietteSupTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { z } from "zod";
import { getAssiettesServitudes, URBANISME_SOURCE } from "../gpf/urbanisme.js";
import { READ_ONLY_OPEN_WORLD_TOOL_ANNOTATIONS } from "../helpers/toolAnnotations.js";
import { featureRefSchema, lonSchema, latSchema } from "../helpers/schemas.js";
import logger from "../logger.js";

// --- Schema ---

Expand Down Expand Up @@ -58,6 +59,10 @@ class AssietteSupTool extends BaseTool<AssietteSupInput> {
* @returns The list of nearby assiettes with optional reusable `feature_ref` metadata.
*/
async execute(input: AssietteSupInput) {
logger.info(`[tool] execute ${this.name} ...`, {
input: input
});

return {
results: await getAssiettesServitudes(input.lon, input.lat),
};
Expand Down
34 changes: 34 additions & 0 deletions src/tools/BaseTool.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,44 @@
import { AsyncLocalStorage } from "node:async_hooks";
import { MCPTool } from "mcp-framework";

import { normalizeToolError } from "../helpers/errors/toolError.js";
import logger from "../logger.js";

const toolInputStorage = new AsyncLocalStorage<Record<string, unknown>>();

export default abstract class BaseTool<TInput extends Record<string, any> = any> extends MCPTool<TInput> {
async toolCall(request: {
params: {
name: string;
arguments?: Record<string, unknown>;
};
}) {
return toolInputStorage.run(request.params.arguments || {}, () => super.toolCall(request));
}

protected createErrorResponse(error: unknown) {
const payload = normalizeToolError(error);
const metadata: Record<string, unknown> = {
tool: this.name,
problem_type: payload.type,
problem_title: payload.title,
error_codes: payload.errors.map((item) => item.code),
};

const toolInput = toolInputStorage.getStore();
if (toolInput !== undefined) {
metadata.input = toolInput;
}

if (payload.upstream?.status !== undefined) {
metadata.upstream_status = payload.upstream.status;
}

if (error instanceof Error && error.name && error.name !== "Error") {
metadata.error_name = error.name;
}

logger.error(`[tool] failed ${this.name}: ${payload.detail}`, metadata);

return {
content: [
Expand Down
5 changes: 5 additions & 0 deletions src/tools/CadastreTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { z } from "zod";
import { getParcellaireExpress, PARCELLAIRE_EXPRESS_TYPES, PARCELLAIRE_EXPRESS_SOURCE } from "../gpf/parcellaire-express.js";
import { READ_ONLY_OPEN_WORLD_TOOL_ANNOTATIONS } from "../helpers/toolAnnotations.js";
import { featureRefSchema, lonSchema, latSchema } from "../helpers/schemas.js";
import logger from "../logger.js";

// --- Schema ---

Expand Down Expand Up @@ -60,6 +61,10 @@ class CadastreTool extends BaseTool<CadastreInput> {
* @returns The nearest cadastral objects, at most one per cadastral type.
*/
async execute(input: CadastreInput) {
logger.info(`[tool] execute ${this.name} ...`, {
input: input
});

return {
results: await getParcellaireExpress(input.lon, input.lat),
};
Expand Down
5 changes: 5 additions & 0 deletions src/tools/GeocodeTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { z } from "zod";

import { geocode, GEOCODE_SOURCE } from "../gpf/geocode.js";
import { READ_ONLY_OPEN_WORLD_TOOL_ANNOTATIONS } from "../helpers/toolAnnotations.js";
import logger from "../logger.js";

// --- Schema ---

Expand Down Expand Up @@ -64,6 +65,10 @@ class GeocodeTool extends BaseTool<GeocodeInput> {
* @returns The ordered list of geocoded candidates.
*/
async execute(input: GeocodeInput) {
logger.info(`[tool] execute ${this.name} ...`, {
input: input
});

return {
results: await geocode(input.text, input.maximumResponses),
};
Expand Down
5 changes: 5 additions & 0 deletions src/tools/GpfWfsDescribeTypeTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type { Collection } from "@ignfab/gpf-schema-store";

import { wfsClient } from "../gpf/wfs-schema-catalog.js";
import { READ_ONLY_OPEN_WORLD_TOOL_ANNOTATIONS } from "../helpers/toolAnnotations.js";
import logger from "../logger.js";

// --- Schema ---

Expand Down Expand Up @@ -64,6 +65,10 @@ class GpfWfsDescribeTypeTool extends BaseTool<GpfWfsDescribeTypeInput> {
* @returns The detailed feature type description from the embedded catalog.
*/
async execute(input: GpfWfsDescribeTypeInput) {
logger.info(`[tool] execute ${this.name} ...`, {
input: input
});

try {
const featureType: Collection = await wfsClient.getFeatureType(input.typename);
return featureType;
Expand Down
5 changes: 5 additions & 0 deletions src/tools/GpfWfsGetFeatureByIdTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
gpfWfsGetFeatureByIdPublishedInputSchema,
gpfWfsGetFeatureByIdRequestOutputSchema,
} from "../helpers/wfs_engine/schema.js";
import logger from "../logger.js";

// --- Tool ---

Expand Down Expand Up @@ -97,6 +98,10 @@ class GpfWfsGetFeatureByIdTool extends BaseTool<GpfWfsGetFeatureByIdInput> {
* @returns Either a compiled request or a transformed FeatureCollection containing one feature.
*/
async execute(input: GpfWfsGetFeatureByIdInput) {
logger.info(`[tool] execute ${this.name} ...`, {
input: input
});

if (input.result_type === "request") {
// The `request` mode is handled here because it returns a preview payload,
// not the actual by-id WFS result.
Expand Down
5 changes: 5 additions & 0 deletions src/tools/GpfWfsGetFeaturesTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
gpfWfsGetFeaturesPublishedInputSchema,
gpfWfsGetFeaturesRequestOutputSchema,
} from "../helpers/wfs_engine/schema.js";
import logger from "../logger.js";

/**
* MCP tool exposing structured WFS feature search.
Expand Down Expand Up @@ -102,6 +103,10 @@ class GpfWfsGetFeaturesTool extends BaseTool<GpfWfsGetFeaturesInput> {
* @returns Either a compiled request, a hit count, or a transformed FeatureCollection.
*/
async execute(input: GpfWfsGetFeaturesInput) {
logger.info(`[tool] execute ${this.name} ...`, {
input: input
});

if (input.result_type === "request") {
const { request } = await prepareGetFeaturesRequest(input);
return toWfsRequestPayload(request);
Expand Down
5 changes: 5 additions & 0 deletions src/tools/GpfWfsSearchTypesTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { z } from "zod";

import { READ_ONLY_OPEN_WORLD_TOOL_ANNOTATIONS } from "../helpers/toolAnnotations.js";
import { wfsClient } from "../gpf/wfs-schema-catalog.js";
import logger from "../logger.js";

// --- Schema ---

Expand Down Expand Up @@ -64,6 +65,10 @@ class GpfWfsSearchTypesTool extends BaseTool<GpfWfsSearchTypesInput> {
* @returns The ordered search results, optionally enriched with relevance scores.
*/
async execute(input: GpfWfsSearchTypesInput) {
logger.info(`[tool] execute ${this.name} ...`, {
input: input
});

const maxResults = input.max_results || 10;
const featureTypes = await wfsClient.searchFeatureTypesWithScores(input.query, maxResults);
return {
Expand Down
5 changes: 5 additions & 0 deletions src/tools/UrbanismeTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { z } from "zod";
import { getUrbanisme, URBANISME_SOURCE } from "../gpf/urbanisme.js";
import { READ_ONLY_OPEN_WORLD_TOOL_ANNOTATIONS } from "../helpers/toolAnnotations.js";
import { featureRefSchema, lonSchema, latSchema } from "../helpers/schemas.js";
import logger from "../logger.js";

// --- Schema ---

Expand Down Expand Up @@ -64,6 +65,10 @@ class UrbanismeTool extends BaseTool<UrbanismeInput> {
* @returns The relevant urban planning objects, including reusable `feature_ref` metadata when available.
*/
async execute(input: UrbanismeInput) {
logger.info(`[tool] execute ${this.name} ...`, {
input: input
});

return {
results: await getUrbanisme(input.lon, input.lat),
};
Expand Down
Loading
Loading