Skip to content
Open
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
105 changes: 65 additions & 40 deletions docs/mcp-tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -956,9 +956,10 @@ Description d’un type GPF
### Description du tool

```
Renvoie le schéma détaillé d'un type GPF à partir de son identifiant (`typename`).
Ce schéma contient notamment la description du type et un champ `properties` qui détaille, pour chaque propriété, son type, sa description et la liste des ses valeurs possibles (`oneOf`) lorsqu'elle est fixée.
Utiliser ce tool après `gpf_search_types` pour inspecter les propriétés disponibles avant d'appeler `gpf_get_features`.
Renvoie un résumé du schéma d'un type GPF à partir de son identifiant (`typename`).
Ce schéma contient notamment la description du type et un champ `properties` qui recense la liste des propriétés avec leur description et la liste de leurs valeurs possibles (`oneOf`) lorsqu'elle est fixée.
Le schéma caractérise aussi la nature de la géométrie des objets du type par le champ `geometry_kind`, à mettre en lien avec les `spatial_extras` calculables dans `gpf_get_features` et `gpf_get_feature_by_id`.
Utiliser ce tool après `gpf_search_types` pour inspecter les propriétés disponibles avant d'appeler `gpf_get_features`. Si le résumé ne suffit pas, télécharger le schéma complet via l'`url` renvoyée.
**IMPORTANT : Appel fortement recommandé si les noms exacts des propriétés ne sont pas connus : un nom de propriété incorrect provoque une erreur**.
```

Expand Down Expand Up @@ -993,16 +994,13 @@ Utiliser ce tool après `gpf_search_types` pour inspecter les propriétés dispo

| Champ | Type | Requis | Description |
| --- | --- | --- | --- |
| `$id` | string | oui | |
| `$schema` | string | oui | |
| `description` | string | oui | |
| `properties` | object | oui | |
| `required` | array | oui | |
| `title` | string | oui | |
| `type` | string | oui | |
| `x-ign-representedFeatures` | array | non | |
| `x-ign-selectionCriteria` | string | non | |
| `x-ign-theme` | string | non | |
| `description` | string | non | La description du contenu du type. |
| `geometry_kind` | string (enum) | non | Le type de la géométrie, si elle existe. Cela peut être un type GeoJSON en minuscules, une union comme "point-or-multipoint" ou encore "any". Ce champ est indéfini lorsque le schéma n'a pas de propriété géométrique. Valeurs : point, multipoint, point-or-multipoint, linestring, multilinestring, linestring-or-multilinestring, polygon, multipolygon, polygon-or-multipolygon, geometrycollection, any. |
| `properties` | array | oui | La liste des propriétés non-géométriques du schéma. |
| `required` | array | oui | La liste des propriétés non-géométriques toujours présentes. Toute propriété qui n'est pas dans cette liste est donc facultative. |
| `selection_criteria` | string | non | Les critères de sélection des objets enregistrés dans ce type. |
| `typename` | string | oui | L'identifiant du type (de la forme `prefixe:nom`). |
| `url` | string | oui | Le lien vers le schéma complet du type, à ne télécharger que lorsque le résumé fourni par `gpf_describe_type` est insuffisant. |

<details>
<summary>Schéma de sortie brut</summary>
Expand All @@ -1011,53 +1009,80 @@ Utiliser ce tool après `gpf_search_types` pour inspecter les propriétés dispo
{
"type": "object",
"properties": {
"$schema": {
"type": "string"
"typename": {
"type": "string",
"description": "L'identifiant du type (de la forme `prefixe:nom`)."
},
"$id": {
"url": {
"type": "string",
"description": "Le lien vers le schéma complet du type, à ne télécharger que lorsque le résumé fourni par `gpf_describe_type` est insuffisant.",
"format": "uri"
},
"type": {
"type": "string"
},
"title": {
"type": "string"
},
"x-ign-theme": {
"type": "string"
},
"description": {
"type": "string"
"type": "string",
"description": "La description du contenu du type."
},
"x-ign-selectionCriteria": {
"type": "string"
"geometry_kind": {
"type": "string",
"description": "Le type de la géométrie, si elle existe. Cela peut être un type GeoJSON en minuscules, une union comme \"point-or-multipoint\" ou encore \"any\". Ce champ est indéfini lorsque le schéma n'a pas de propriété géométrique.",
"enum": [
"point",
"multipoint",
"point-or-multipoint",
"linestring",
"multilinestring",
"linestring-or-multilinestring",
"polygon",
"multipolygon",
"polygon-or-multipolygon",
"geometrycollection",
"any"
]
},
"x-ign-representedFeatures": {
"properties": {
"type": "array",
"description": "La liste des propriétés non-géométriques du schéma.",
"items": {
"type": "string"
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Le nom de la propriété."
},
"description": {
"type": "string",
"description": "La description de la propriété."
},
"oneOf": {
"type": "array",
"description": "La liste des valeurs possibles, si elle existe.",
"items": {
"type": "string"
}
}
},
"required": [
"name"
]
}
},
"required": {
"type": "array",
"description": "La liste des propriétés non-géométriques toujours présentes. Toute propriété qui n'est pas dans cette liste est donc facultative.",
"items": {
"type": "string"
}
},
"properties": {
"type": "object",
"properties": {}
"selection_criteria": {
"type": "string",
"description": "Les critères de sélection des objets enregistrés dans ce type."
}
},
"required": [
"$schema",
"$id",
"type",
"title",
"description",
"required",
"properties"
"typename",
"url",
"properties",
"required"
]
}
```
Expand Down
102 changes: 88 additions & 14 deletions src/tools/GpfDescribeTypeTool.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
/**
* MCP tool exposing detailed schema inspection for a single WFS type.
* MCP tool exposing a summarized schema for a single WFS type.
*/

import BaseTool from "./BaseTool.js";
import { z } from "zod";
import { zOgcCollectionSchema } from "@ignfab/gpf-schema-store";

import { wfsSchemaStore } from "../wfs/catalog.js";
import type { OgcCollectionPropertyEnumValue } from "@ignfab/gpf-schema-store";
import { type GpfFeatureType, wfsSchemaStore } from "../wfs/catalog.js";
import { READ_ONLY_CLOSED_WORLD_TOOL_ANNOTATIONS } from "../helpers/toolAnnotations.js";
import logger from "../logger.js";
import { getGeometryName, getGeometryProperties } from "../wfs/properties.js";

// --- Schemas ---

Expand All @@ -20,22 +21,81 @@ const gpfDescribeTypeInputSchema = z.object({
.describe("Le nom du type à décrire (de la forme `prefixe:nom`)."),
}).strict();

// FIXME: when mcp-framework is removed, remove this patch which is only here
// because mcp-framework does not accept z.record field types.
const gpfDescribeTypeOutputSchema = zOgcCollectionSchema
.omit({ properties: true })
.extend({ properties: z.object({}).catchall(z.unknown()) });
const gpfPropertySchema = z.object({
name: z.string().describe("Le nom de la propriété."),
description: z.string().optional().describe("La description de la propriété."),
oneOf: z.array(z.string()).optional().describe("La liste des valeurs possibles, si elle existe.")
});

const ogcGeometryKind = [
"point",
"multipoint",
"point-or-multipoint",
"linestring",
"multilinestring",
"linestring-or-multilinestring",
"polygon",
"multipolygon",
"polygon-or-multipolygon",
"geometrycollection",
"any"
] as const;

const gpfDescribeTypeOutputSchema = z.object({
typename: z.string().describe("L'identifiant du type (de la forme `prefixe:nom`)."),
url: z.string().url().describe("Le lien vers le schéma complet du type, à ne télécharger que lorsque le résumé fourni par `gpf_describe_type` est insuffisant."),
description: z.string().optional().describe("La description du contenu du type."),
geometry_kind: z.enum(ogcGeometryKind).optional().describe("Le type de la géométrie, si elle existe. Cela peut être un type GeoJSON en minuscules, une union comme \"point-or-multipoint\" ou encore \"any\". Ce champ est indéfini lorsque le schéma n'a pas de propriété géométrique."),
properties: z.array(gpfPropertySchema).describe("La liste des propriétés non-géométriques du schéma."),
required: z.array(z.string()).describe("La liste des propriétés non-géométriques toujours présentes. Toute propriété qui n'est pas dans cette liste est donc facultative."),
selection_criteria: z.string().optional().describe("Les critères de sélection des objets enregistrés dans ce type."),
});

// --- Types ---

type GpfDescribeTypeInput = z.infer<typeof gpfDescribeTypeInputSchema>;
type GpfDescribeTypeOutput = z.infer<typeof gpfDescribeTypeOutputSchema>;

// --- Utility ---

function summarizeSchema(featureType: GpfFeatureType) : GpfDescribeTypeOutput {
const schema = featureType.schema;
const geometricPropertyNames = getGeometryProperties(featureType);
const geometryName = geometricPropertyNames.length > 0 ? getGeometryName(featureType) : undefined;
const format = geometryName ? schema.properties[geometryName].format : undefined;
const kind = format?.replace(/^geometry-/, "");
const shortProperties = Object.keys(schema.properties)
.filter(name => !geometricPropertyNames.includes(name))
.map(name => {
const property = schema.properties[name];
return {
name,
description: property.description,
oneOf: property.oneOf?.map((v: OgcCollectionPropertyEnumValue) => v.const),
};
});
const required = schema.required.filter(
(name: string) => !geometricPropertyNames.includes(name),
);

return {
typename: featureType.typename,
url: schema.$id,
description: schema.description,
geometry_kind: ogcGeometryKind.find(k => k === kind),
properties: shortProperties,
required,
selection_criteria: schema["x-ign-selectionCriteria"],
};
}

// --- Tool ---

const GPF_DESCRIBE_TYPE_TOOL_DESCRIPTION = [
"Renvoie le schéma détaillé d'un type GPF à partir de son identifiant (`typename`).",
"Ce schéma contient notamment la description du type et un champ `properties` qui détaille, pour chaque propriété, son type, sa description et la liste des ses valeurs possibles (`oneOf`) lorsqu'elle est fixée.",
"Utiliser ce tool après `gpf_search_types` pour inspecter les propriétés disponibles avant d'appeler `gpf_get_features`.",
"Renvoie un résumé du schéma d'un type GPF à partir de son identifiant (`typename`).",
"Ce schéma contient notamment la description du type et un champ `properties` qui recense la liste des propriétés avec leur description et la liste de leurs valeurs possibles (`oneOf`) lorsqu'elle est fixée.",
"Le schéma caractérise aussi la nature de la géométrie des objets du type par le champ `geometry_kind`, à mettre en lien avec les `spatial_extras` calculables dans `gpf_get_features` et `gpf_get_feature_by_id`.",
"Utiliser ce tool après `gpf_search_types` pour inspecter les propriétés disponibles avant d'appeler `gpf_get_features`. Si le résumé ne suffit pas, télécharger le schéma complet via l'`url` renvoyée.",
"**IMPORTANT : Appel fortement recommandé si les noms exacts des propriétés ne sont pas connus : un nom de propriété incorrect provoque une erreur**."
].join("\n");

Expand All @@ -49,10 +109,24 @@ class GpfDescribeTypeTool extends BaseTool<GpfDescribeTypeInput> {
schema = gpfDescribeTypeInputSchema;

/**
* Loads the detailed schema description for one GPF typename.
* Formats the summary payload into both text content and structuredContent.
*
* @param data Raw execution result.
* @returns An MCP success response with validated output shape.
*/
protected createSuccessResponse(data: unknown) {
const payload = gpfDescribeTypeOutputSchema.parse(data);
return {
content: [{ type: "text" as const, text: JSON.stringify(payload) }],
structuredContent: payload,
};
}

/**
* Loads and summarizes the schema description for one GPF typename.
*
* @param input Normalized tool input.
* @returns The detailed feature type description from the embedded catalog.
* @returns The summarized feature type description from the embedded catalog.
*/
async execute(input: GpfDescribeTypeInput) {
logger.info(`[tool] execute ${this.name} ...`, {
Expand All @@ -61,7 +135,7 @@ class GpfDescribeTypeTool extends BaseTool<GpfDescribeTypeInput> {

try {
const featureType = await wfsSchemaStore.getFeatureType(input.typename);
return featureType.schema;
return summarizeSchema(featureType);
} catch (e: unknown) {
const message = e instanceof Error ? e.message : String(e);
throw new Error(`${message}. Utiliser gpf_search_types pour trouver un type valide.`);
Expand Down
2 changes: 1 addition & 1 deletion src/wfs/properties.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { GPF_GET_FEATURES_SPATIAL_EXTRAS, type SpatialExtraOptions } from "./sch
* @param featureType Feature type definition loaded from the embedded catalog.
* @returns The list of spatial properties.
*/
function getGeometryProperties(featureType: GpfFeatureType) {
export function getGeometryProperties(featureType: GpfFeatureType) {
return Object.entries(featureType.schema.properties).filter(([_key, property]) => {
// only geometric properties do not have a `type` field
// (see OGC API Features, /req/schemas/properties A and B)
Expand Down
26 changes: 13 additions & 13 deletions test/integration/level1-protocol/describe.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,16 @@ import { expectToolCallToThrow } from "../helpers/level1-assertions.js";
import { INTEGRATION_CONFIG } from "../config/shared.js";

interface DescribeResult {
title: string;
typename: string;
url: string;
description: string;
geometry_kind?: string;
required: string[];
properties: Record<string, {
type?: "string" | "boolean" | "integer" | "number";
title?: string;
selection_criteria?: string;
properties: Array<{
name: string;
description?: string;
oneOf?: Array<{
const: string;
title: string;
description?: string;
}>;
oneOf?: string[];
Comment thread
LionelZoubritzky-IGN marked this conversation as resolved.
}>;
}

Expand All @@ -32,11 +30,13 @@ describe("GPF Describe Type (integration)", () => {
typename: "BDTOPO_V3:batiment",
});

expect(result.title).toBe("Bâtiment");
expect(result.typename).toBe("BDTOPO_V3:batiment");
expect(result.url).toContain("BDTOPO_V3");
expect(Array.isArray(result.required)).toBe(true);
expect(result.selection_criteria).toBeTypeOf("string")
expect(result.properties).toBeDefined();
const propNames = Object.keys(result.properties);
expect(propNames.length).toBeGreaterThan(0);
expect(result.required).toBeDefined();
expect(result.properties.length).toBeGreaterThan(0);
expect(result.properties[0].name).toBeDefined();
}, INTEGRATION_CONFIG.timeout);

it("should return an error for empty typename", async () => {
Expand Down
Loading
Loading