diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml
index 195e600..ea17cae 100644
--- a/.github/workflows/node.js.yml
+++ b/.github/workflows/node.js.yml
@@ -2,9 +2,9 @@ name: Node.js CI
on:
push:
- branches: [ "main" ]
+ branches: [ "main", "release/**" ]
pull_request:
- branches: [ "main" ]
+ branches: [ "main", "release/**" ]
jobs:
build:
diff --git a/.gitignore b/.gitignore
index 2e8aeb9..fefa1c6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,4 +5,5 @@
/.vscode/
.npmrc
.codex
-.env.e2e
\ No newline at end of file
+.env.e2e
+.continue/
diff --git a/README.md b/README.md
index 90c6c44..5afa698 100644
--- a/README.md
+++ b/README.md
@@ -190,6 +190,7 @@ Les fonctionnalités correspondent aux outils MCP documentés dans [`docs/mcp-to
| Récupérer le cadastre | `cadastre` | [WFS](https://cartes.gouv.fr/aide/fr/guides-utilisateur/utiliser-les-services-de-la-geoplateforme/diffusion/wfs/) + [PARCELLAIRE-EXPRESS](https://cartes.gouv.fr/rechercher-une-donnee/dataset/IGNF_PARCELLAIRE-EXPRESS-PCI) | Parcelle cadastrale |
| Récupérer les documents d'urbanisme | `urbanisme` | [WFS](https://cartes.gouv.fr/aide/fr/guides-utilisateur/utiliser-les-services-de-la-geoplateforme/diffusion/wfs/) + [données GPU](https://www.geoportail-urbanisme.gouv.fr/) | PLU, POS, CC |
| Récupérer les servitudes | `assiette_sup` | [WFS](https://cartes.gouv.fr/aide/fr/guides-utilisateur/utiliser-les-services-de-la-geoplateforme/diffusion/wfs/) + [données GPU](https://www.geoportail-urbanisme.gouv.fr/) | SUP autour d'un lieu |
+| Trouver les points d'intérêt proches| `pointsdinteret` | [Géocodage Géoplateforme](https://cartes.gouv.fr/aide/fr/guides-utilisateur/utiliser-les-services-de-la-geoplateforme/geocodage/)
| Trouver une couche WFS | `gpf_wfs_search_types` | [gpf-schema-store](https://github.com/ignfab/gpf-schema-store) | Trouver la table des bâtiments |
| Décrire une couche WFS | `gpf_wfs_describe_type` | [gpf-schema-store](https://github.com/ignfab/gpf-schema-store) | Lister les champs disponibles |
| Interroger une couche WFS | `gpf_wfs_get_features` | [WFS](https://cartes.gouv.fr/aide/fr/guides-utilisateur/utiliser-les-services-de-la-geoplateforme/diffusion/wfs/) | Extraire ou compter des objets |
diff --git a/docs/mcp-tools.md b/docs/mcp-tools.md
index 28e5a59..4f970bc 100644
--- a/docs/mcp-tools.md
+++ b/docs/mcp-tools.md
@@ -61,6 +61,7 @@ Tous les tools exposent les mêmes annotations MCP dans leur définition `tools/
- [`gpf_wfs_describe_type`](#gpf_wfs_describe_type)
- [`gpf_wfs_get_feature_by_id`](#gpf_wfs_get_feature_by_id)
- [`gpf_wfs_get_features`](#gpf_wfs_get_features)
+- [`pointsdinteret`](#pointsdinteret)
## `geocode`
@@ -1492,3 +1493,153 @@ Aucun `outputSchema` unique n'est exposé. La sortie dépend de `result_type` (`
| Succès `result_type="http_post_request"` | oui | oui | `content[0].text` est `JSON.stringify(structuredContent)`. |
| Succès `result_type="http_get_url"` | oui | oui | `content[0].text` est `JSON.stringify(structuredContent)`. |
| Erreur | oui | oui | `content[0].text` contient `structuredContent.detail`, pas le JSON d'erreur complet de `structuredContent`. |
+
+## `pointsdinteret`
+
+Code Source : [src/tools/PointsDInteretTool.ts](../src/tools/PointsDInteretTool.ts)
+
+### Titre
+
+Points d'intérêt obtenus par géocodage inverse
+
+### Description du tool
+
+```
+Renvoie les points d'intérêt les plus proches des coordonnées en entrée.
+Le champ `name` contient le nom du point d'intérêt et le champ `categories` liste ses classifications.
+Chaque résultat peut aussi inclure les coordonnées du point d'intérêt (`centroid`), sa distance aux coordonnées de départ (`distance`), ainsi que des informations de localisation (`city`, `zipcode`).
+Les réultats sont classés par distance, puis par importance : utilisez des coordonnées précises et montez la valeur de `maximumResponses` si l'information ne semble pas assez pertinente.
+Pour obtenir un résultat plus détaillé sur un point d'intérêt trouvé, appelez ensuite `wfs_search_types` avec des éléments pertinents de `category`, puis `wfs_get_features` avec le `typename` obtenu et les coordonnées du centroïde.
+(source : Géoplateforme (service de géocodage)).
+```
+
+### Schéma d’entrée
+
+| Champ | Type | Requis | Description |
+| --- | --- | --- | --- |
+| `lat` | number | oui | La latitude du point. |
+| `lon` | number | oui | La longitude du point. |
+| `maximumResponses` | integer | non | Le nombre maximum de résultats à retourner (entre 1 et 20). Défaut : 3. |
+
+
+Schéma d’entrée brut
+
+```json
+{
+ "type": "object",
+ "properties": {
+ "lon": {
+ "type": "number",
+ "description": "La longitude du point.",
+ "minimum": -180,
+ "maximum": 180
+ },
+ "lat": {
+ "type": "number",
+ "description": "La latitude du point.",
+ "minimum": -90,
+ "maximum": 90
+ },
+ "maximumResponses": {
+ "type": "integer",
+ "description": "Le nombre maximum de résultats à retourner (entre 1 et 20). Défaut : 3.",
+ "minimum": 1,
+ "maximum": 20
+ }
+ },
+ "required": [
+ "lon",
+ "lat"
+ ]
+}
+```
+
+
+
+### Schéma de sortie
+
+| Champ | Type | Requis | Description |
+| --- | --- | --- | --- |
+| `results` | array | oui | La liste des points d'intérêt à proximité, ordonnée par distance. |
+
+
+Schéma de sortie brut
+
+```json
+{
+ "type": "object",
+ "properties": {
+ "results": {
+ "type": "array",
+ "description": "La liste des points d'intérêt à proximité, ordonnée par distance.",
+ "items": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "Le nom du point d'intérêt trouvé."
+ },
+ "categories": {
+ "type": "array",
+ "description": "Les catégories du point d'intérêt.",
+ "items": {
+ "type": "string"
+ }
+ },
+ "city": {
+ "type": "string",
+ "description": "Le nom de la ville où est le point d'intérêt."
+ },
+ "zipcode": {
+ "type": "string",
+ "description": "Le code postal du point d'intérêt"
+ },
+ "distance": {
+ "type": "number",
+ "description": "La distance en mètres entre le point demandé et le point d'intérêt retenu."
+ },
+ "centroid": {
+ "type": "object",
+ "description": "Les coordonnées du centre du point d'intérêt.",
+ "properties": {
+ "lon": {
+ "type": "number",
+ "description": "La longitude du point.",
+ "minimum": -180,
+ "maximum": 180
+ },
+ "lat": {
+ "type": "number",
+ "description": "La latitude du point.",
+ "minimum": -90,
+ "maximum": 90
+ }
+ },
+ "required": [
+ "lon",
+ "lat"
+ ]
+ }
+ },
+ "required": [
+ "name",
+ "categories",
+ "distance"
+ ]
+ }
+ }
+ },
+ "required": [
+ "results"
+ ]
+}
+```
+
+
+
+### Réponse MCP
+
+| Cas | `content` | `structuredContent` | Relation entre `content` et `structuredContent` |
+| --- | --- | --- | --- |
+| Succès | oui | oui | `content[0].text` est `JSON.stringify(structuredContent)`. |
+| Erreur | oui | oui | `content[0].text` contient `structuredContent.detail`, pas le JSON d'erreur complet de `structuredContent`. |
diff --git a/src/gpf/pointsdinteret.ts b/src/gpf/pointsdinteret.ts
new file mode 100644
index 0000000..edbd475
--- /dev/null
+++ b/src/gpf/pointsdinteret.ts
@@ -0,0 +1,79 @@
+import { fetchJSONGet } from "../helpers/http.js";
+import logger from "../logger.js";
+import type { JsonFetcher } from "../helpers/http.js";
+import { RateLimiter } from "../helpers/RateLimiter.js";
+import { getEnv } from "../config/env.js";
+
+export const POINTSDINTERET_SOURCE = "Géoplateforme (service de géocodage)";
+
+type RawPointsDInteretFeature = {
+ properties: {
+ toponym: string;
+ category: string[];
+ city?: string[];
+ postcode?: string[];
+ distance: number;
+ };
+ geometry: {
+ type: string;
+ coordinates: number[];
+ };
+}
+
+export type PointsDInteretResult = {
+ name: string;
+ categories: string[];
+ city?: string;
+ zipcode?: string;
+ distance: number;
+ centroid?: {
+ lon: number,
+ lat: number
+ };
+};
+
+type RawPointsDInteretResponse = {
+ features?: RawPointsDInteretFeature[];
+};
+
+export class PointsDInteretClient {
+ constructor(
+ private rateLimiter: RateLimiter,
+ private fetcher: JsonFetcher = fetchJSONGet,
+ ) {}
+
+ /**
+ * Get the nearest points of interest for given coordinates
+ *
+ * @see https://geoservices.ign.fr/documentation/services/services-geoplateforme/geocodage
+ */
+ async pointsdinteret(lon: number, lat: number, maximumResponses = 3): Promise {
+ await this.rateLimiter.limit();
+ logger.debug(`[gpf:pointsdinteret] pointsdinteret(${lon}, ${lat}, ${maximumResponses})...`);
+
+ const url = 'https://data.geopf.fr/geocodage/reverse/?' + new URLSearchParams({
+ lon: String(lon),
+ lat: String(lat),
+ index: "poi", // we could also include "parcel" but it is redundant with the cadastre tool
+ limit: String(maximumResponses),
+ }).toString();
+
+ const json: RawPointsDInteretResponse = await this.fetcher(url);
+ const results = Array.isArray(json?.features) ? json.features : [];
+ return results.map((item) => ({
+ name: item.properties.toponym,
+ categories: item.properties.category,
+ city: Array.isArray(item.properties.city) ? item.properties.city[0] : undefined,
+ zipcode: Array.isArray(item.properties.postcode) ? item.properties.postcode[0] : undefined,
+ distance: item.properties.distance,
+ centroid: item.geometry.type == "Point" ? {
+ lon: item.geometry.coordinates[0],
+ lat: item.geometry.coordinates[1]
+ } : undefined,
+ }));
+ }
+}
+
+export const pointsdinteretClient = new PointsDInteretClient(
+ new RateLimiter({ name: "GPF_POINTSDINTERET", maxCalls: getEnv().GPF_GEOCODE_RATE_LIMIT, period: 1 }),
+);
diff --git a/src/tools/PointsDInteretTool.ts b/src/tools/PointsDInteretTool.ts
new file mode 100644
index 0000000..6df402d
--- /dev/null
+++ b/src/tools/PointsDInteretTool.ts
@@ -0,0 +1,84 @@
+/**
+ * MCP tool exposing reverse geocoding for points of interest.
+ */
+
+import BaseTool from "./BaseTool.js";
+import { z } from "zod";
+
+import { pointsdinteretClient, POINTSDINTERET_SOURCE } from "../gpf/pointsdinteret.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 ---
+
+const pointsdinteretInputSchema = z.object({
+ lon: lonSchema,
+ lat: latSchema,
+ maximumResponses: z
+ .number()
+ .int()
+ .min(1)
+ .max(50)
+ .optional()
+ .describe("Le nombre maximum de résultats à retourner (entre 1 et 50). Défaut : 3."),
+}).strict();
+
+// --- Types ---
+
+type PointsDInteretInput = z.infer;
+
+const pointsdinteretResultSchema = z
+ .object({
+ name: z.string().describe("Le nom du point d'intérêt trouvé"),
+ categories: z.array(z.string()).describe("Ses catégories"),
+ city: z.string().optional().describe("Sa ville"),
+ zipcode: z.string().optional().describe("Son code postal"),
+ distance: z.number().describe("La distance en mètres entre le point demandé et le point d'intérêt retenu"),
+ centroid: z.object({
+ lon: lonSchema,
+ lat: latSchema
+ }).optional().describe("Les coordonnées du centre du point d'intérêt")
+})
+.catchall(z.unknown());
+
+const pointsdinteretOutputSchema = z.object({
+ results: z.array(pointsdinteretResultSchema).describe("La liste des points d'intérêt à proximité, ordonnée par distance."),
+});
+
+// --- Tool ---
+
+class PointsDInteretTool extends BaseTool {
+ name = "pointsdinteret";
+ title = "Points d'intérêt obtenus par géocodage inverse";
+ annotations = READ_ONLY_OPEN_WORLD_TOOL_ANNOTATIONS;
+ description = [
+ "Renvoie les points d'intérêt les plus proches des coordonnées en entrée.",
+ "Le champ `name` contient le nom du point d'intérêt et le champ `categories` liste ses classifications.",
+ "Chaque résultat peut aussi inclure les coordonnées du point d'intérêt (`centroid`), sa distance aux coordonnées de départ (`distance`), ainsi que des informations de localisation (`city`, `zipcode`).",
+ "Les réultats sont classés par distance, puis par importance : utilisez des coordonnées précises et montez la valeur de `maximumResponses` si l'information ne semble pas assez pertinente.",
+ "Pour obtenir un résultat plus détaillé sur un point d'intérêt trouvé, appelez ensuite `wfs_search_types` avec des éléments pertinents de `category`, puis `wfs_get_features` avec le `typename` obtenu et les coordonnées du centroïde.",
+ `(source : ${POINTSDINTERET_SOURCE}).`
+ ].join("\n");
+ protected outputSchemaShape = pointsdinteretOutputSchema;
+
+ schema = pointsdinteretInputSchema;
+
+ /**
+ * Returns the points of interest relevant to the requested point.
+ *
+ * @param input Normalized tool input.
+ * @returns The relevant points of interest.
+ */
+ async execute(input: PointsDInteretInput) {
+ logger.info(`[tool] execute ${this.name} ...`, {
+ input: input
+ });
+
+ return {
+ results: await pointsdinteretClient.pointsdinteret(input.lon, input.lat, input.maximumResponses),
+ };
+ }
+}
+
+export default PointsDInteretTool;
diff --git a/test/gpf/pointsdinteret.test.ts b/test/gpf/pointsdinteret.test.ts
new file mode 100644
index 0000000..f176bf7
--- /dev/null
+++ b/test/gpf/pointsdinteret.test.ts
@@ -0,0 +1,167 @@
+import { describe, expect, it } from "vitest";
+import { PointsDInteretClient } from "../../src/gpf/pointsdinteret.js";
+import { RateLimiter } from "../../src/helpers/RateLimiter.js";
+
+const rawPointsDInteretServiceResponse = {
+ "type": "FeatureCollection",
+ "features": [
+ {
+ "type": "Feature",
+ "geometry": {
+ "type": "Point",
+ "coordinates": [
+ 45.10948,
+ -12.855996
+ ]
+ },
+ "properties": {
+ "name": [
+ "Pengoua Bolé"
+ ],
+ "toponym": "Pengoua Bolé",
+ "category": [
+ "sommet",
+ "élément topographique ou forestier",
+ "détail orographique"
+ ],
+ "classification": 8,
+ "importance": 0.3,
+ "extrafields": {
+ "cleabs": "PAIOROGR0000001600001372"
+ },
+ "citycode": [
+ "97616"
+ ],
+ "depcode": [
+ "976"
+ ],
+ "city": [
+ "Sada"
+ ],
+ "postcode": [
+ "97640"
+ ],
+ "territory": "DOMTOM",
+ "distance": 30,
+ "score": 0.997,
+ "_type": "poi"
+ }
+ },
+ {
+ "type": "Feature",
+ "geometry": {
+ "type": "Point",
+ "coordinates": [
+ 45.110309,
+ -12.859327
+ ]
+ },
+ "properties": {
+ "name": [
+ "Sada"
+ ],
+ "toponym": "Sada",
+ "category": [
+ "administratif",
+ "commune"
+ ],
+ "postcode": [
+ "97640"
+ ],
+ "citycode": [
+ "97616"
+ ],
+ "depcode": [
+ "976"
+ ],
+ "classification": 2,
+ "importance": 0.9,
+ "extrafields": {
+ "population": "11156",
+ "status": "",
+ "cleabs": "COMMUNE_0000001600043245"
+ },
+ "territory": "DOMTOM",
+ "distance": 407,
+ "score": 0.9593,
+ "_type": "poi"
+ }
+ },
+ {
+ "type": "Feature",
+ "geometry": {
+ "type": "Point",
+ "coordinates": [
+ 45.101193,
+ -12.805296
+ ]
+ },
+ "properties": {
+ "name": [
+ "CC du Centre-Ouest"
+ ],
+ "toponym": "CC du Centre-Ouest",
+ "category": [
+ "administratif",
+ "epci"
+ ],
+ "classification": 2,
+ "importance": 0.9,
+ "extrafields": {
+ "codes_insee_des_communes_membres": [
+ "97617",
+ "97616",
+ "97614",
+ "97605",
+ "97613"
+ ],
+ "cleabs": "EPCI____0000002150236238"
+ },
+ "territory": "DOMTOM",
+ "distance": 5684,
+ "score": 0.4316,
+ "_type": "poi"
+ }
+ }
+ ]
+};
+
+describe("Test PointsDInteretClient.pointsdinteret",() => {
+ it("should identify 'Pengoua Bolé'", async () => {
+ const rateLimiter = new RateLimiter({ name: "test", maxCalls: 100, period: 1 });
+ const client = new PointsDInteretClient(rateLimiter, async () => ({
+ features: [rawPointsDInteretServiceResponse.features[0], rawPointsDInteretServiceResponse.features[1]],
+ }));
+ const results = await client.pointsdinteret(45.104692, -12.84725, 3);
+ expect(results.length).toBeGreaterThan(0);
+ const firstItem = results[0];
+
+ expect(firstItem.name).toEqual("Pengoua Bolé");
+ expect(firstItem.centroid?.lon).toBeCloseTo(45.10948);
+ expect(firstItem.centroid?.lat).toBeCloseTo(-12.855996);
+ expect(firstItem.categories).toEqual(["sommet", "élément topographique ou forestier", "détail orographique"]);
+ expect(firstItem.city).toEqual('Sada');
+ expect(firstItem.zipcode).toEqual('97640');
+ expect(firstItem.distance).toBeCloseTo(30)
+
+ });
+
+ it("should honor maximumResponses", async () => {
+ const rateLimiter = new RateLimiter({ name: "test", maxCalls: 100, period: 1 });
+ const client = new PointsDInteretClient(rateLimiter, async () => ({
+ features: [rawPointsDInteretServiceResponse.features[2]],
+ }));
+ const results = await client.pointsdinteret(45.101193, -12.805296, 1);
+
+ expect(results).toHaveLength(1);
+ });
+
+ it("should return an empty array for unknown points", async () => {
+ const rateLimiter = new RateLimiter({ name: "test", maxCalls: 100, period: 1 });
+ const client = new PointsDInteretClient(rateLimiter, async () => ({ features: [] }));
+ const results = await client.pointsdinteret(-135.645895, -39.143907);
+
+ expect(results).toEqual([]);
+ });
+
+});
diff --git a/test/integration/level1-protocol/pointsdinteret.test.ts b/test/integration/level1-protocol/pointsdinteret.test.ts
new file mode 100644
index 0000000..110800a
--- /dev/null
+++ b/test/integration/level1-protocol/pointsdinteret.test.ts
@@ -0,0 +1,58 @@
+/**
+ * Integration test: geocode tool with real API calls.
+ */
+
+import { describe, it, expect } from "vitest";
+import { callTool } from "../helpers/mcp-client.js";
+import { withMcpServer } from "../helpers/level1-fixtures.js";
+import { expectNonEmptyResults, expectToolCallToThrow } from "../helpers/level1-assertions.js";
+import { INTEGRATION_CONFIG } from "../config/shared.js";
+
+export type PointsDInteretResult = {
+ results: Array<{
+ name: string;
+ categories: string[];
+ city?: string;
+ zipcode?: string;
+ distance: number;
+ centroid?: {
+ lon: number,
+ lat: number
+ };
+ }>;
+};
+
+
+describe("Geocode Tool (integration)", () => {
+ const { getHandle } = withMcpServer();
+
+ it("should find a result for a point in a Saint-Pierre (Réunion)", async () => {
+ const result = await callTool(getHandle().client, "pointsdinteret", {
+ lon: 55.482554,
+ lat: -20.904138,
+ maximumResponses: 1,
+ });
+
+ expectNonEmptyResults(result);
+
+ const first = result.results[0];
+ expect(first.name).toBeDefined();
+ expect(first.categories).toBeDefined();
+ expect(first.distance).toBeDefined();
+ }, INTEGRATION_CONFIG.timeout);
+
+ it("should find the Refuge de la Femma from approximate coordinates", async () => {
+ const result = await callTool(getHandle().client, "pointsdinteret", {
+ lon: 6.929378,
+ lat: 45.362713,
+ maximumResponses: 5,
+ });
+
+ expectNonEmptyResults(result);
+ const text = JSON.stringify(result).toLowerCase();
+ expect(text).toContain("refuge de la femma");
+ expect(text).toContain("val-cenis");
+ expect(text).toContain("savoie");
+ expect(text).toContain("auvergne-rhône-alpes");
+ }, INTEGRATION_CONFIG.timeout);
+});
diff --git a/test/integration/level2-agent/level2-agent.test.ts b/test/integration/level2-agent/level2-agent.test.ts
index 3a4f296..306e72c 100644
--- a/test/integration/level2-agent/level2-agent.test.ts
+++ b/test/integration/level2-agent/level2-agent.test.ts
@@ -65,6 +65,13 @@ const mcpScenarios = [
expect(containsNumberInRange(normalizedFinalMessage, 1000, 1100)).toBe(true);
},
},
+ {
+ testName: "should chain geocode and pointsofinterest tools to answer the question",
+ userInput: "Qu'est-ce qui se trouve exactement à mi-chemin entre la place de la Contrescarpe et le Centre Pompidou ?",
+ expectedResponseFragments: ["Trésor", "Notre-Dame"],
+ toolMode: "mcp",
+ requiredToolCalls: ["geocode", "pointsofinterest"],
+ },
{
testName: "should answer the question about 14 lycées near the chateau de Vincennes",
userInput: "Combien de lycées sont situés à 2km du chateau de vincennes?",
diff --git a/test/integration/samples.ts b/test/integration/samples.ts
index 5fda541..274180d 100644
--- a/test/integration/samples.ts
+++ b/test/integration/samples.ts
@@ -19,6 +19,7 @@ export const EXPECTED_TOOL_NAMES = [
"cadastre",
"urbanisme",
"assiette_sup",
+ "pointsdinteret",
"gpf_wfs_search_types",
"gpf_wfs_describe_type",
"gpf_wfs_get_features",
diff --git a/test/tools/strict-input.test.ts b/test/tools/strict-input.test.ts
index c541a3a..3887ff4 100644
--- a/test/tools/strict-input.test.ts
+++ b/test/tools/strict-input.test.ts
@@ -9,6 +9,7 @@ import GpfWfsDescribeTypeTool from "../../src/tools/GpfWfsDescribeTypeTool";
import GpfWfsGetFeatureByIdTool from "../../src/tools/GpfWfsGetFeatureByIdTool";
import GpfWfsGetFeaturesTool from "../../src/tools/GpfWfsGetFeaturesTool";
import GpfWfsSearchTypesTool from "../../src/tools/GpfWfsSearchTypesTool";
+import PointsDInteretTool from "../../src/tools/PointsDInteretTool";
import UrbanismeTool from "../../src/tools/UrbanismeTool";
const strictInputCases = [
@@ -65,6 +66,11 @@ const strictInputCases = [
tool: new UrbanismeTool(),
validArguments: { lon: 2.3522, lat: 48.8566 },
},
+ {
+ label: "PointsDInteretTool",
+ tool: new PointsDInteretTool(),
+ validArguments: { lon: 2.3522, lat: 48.8566 },
+ }
] as const;
describe("Strict tool input schemas", () => {