@honua/mcp-server 0.1.2-beta.0 → 0.1.4-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +202 -7
  3. package/dist/src/certification/census-fixture-client.d.ts.map +1 -1
  4. package/dist/src/certification/census-fixture-client.js +5 -0
  5. package/dist/src/certification/certifier.d.ts +1 -1
  6. package/dist/src/certification/certifier.d.ts.map +1 -1
  7. package/dist/src/certification/certifier.js +28 -20
  8. package/dist/src/certification/cli.js +1 -1
  9. package/dist/src/certification/cql2.d.ts +24 -0
  10. package/dist/src/certification/cql2.d.ts.map +1 -0
  11. package/dist/src/certification/cql2.js +354 -0
  12. package/dist/src/certification/fixtures.d.ts +8 -0
  13. package/dist/src/certification/fixtures.d.ts.map +1 -1
  14. package/dist/src/certification/fixtures.js +26 -10
  15. package/dist/src/certification/ogc-data.d.ts +30 -0
  16. package/dist/src/certification/ogc-data.d.ts.map +1 -0
  17. package/dist/src/certification/ogc-data.js +726 -0
  18. package/dist/src/certification/ogc-fixture-client.d.ts +4 -0
  19. package/dist/src/certification/ogc-fixture-client.d.ts.map +1 -0
  20. package/dist/src/certification/ogc-fixture-client.js +190 -0
  21. package/dist/src/certification/target.d.ts +10 -1
  22. package/dist/src/certification/target.d.ts.map +1 -1
  23. package/dist/src/certification/target.js +45 -2
  24. package/dist/src/eval/cli.js +6 -3
  25. package/dist/src/eval/corpus.d.ts +7 -0
  26. package/dist/src/eval/corpus.d.ts.map +1 -1
  27. package/dist/src/eval/corpus.js +12 -0
  28. package/dist/src/eval/ogc-corpus.d.ts +3 -0
  29. package/dist/src/eval/ogc-corpus.d.ts.map +1 -0
  30. package/dist/src/eval/ogc-corpus.js +437 -0
  31. package/dist/src/eval/runner.d.ts +6 -0
  32. package/dist/src/eval/runner.d.ts.map +1 -1
  33. package/dist/src/eval/runner.js +21 -7
  34. package/dist/src/helpers.d.ts +8 -13
  35. package/dist/src/helpers.d.ts.map +1 -1
  36. package/dist/src/helpers.js +7 -35
  37. package/dist/src/index.d.ts +14 -7
  38. package/dist/src/index.d.ts.map +1 -1
  39. package/dist/src/index.js +24 -15
  40. package/dist/src/neutral/errors.d.ts +73 -0
  41. package/dist/src/neutral/errors.d.ts.map +1 -0
  42. package/dist/src/neutral/errors.js +112 -0
  43. package/dist/src/neutral/filter.d.ts +108 -0
  44. package/dist/src/neutral/filter.d.ts.map +1 -0
  45. package/dist/src/neutral/filter.js +271 -0
  46. package/dist/src/neutral/query.d.ts +101 -0
  47. package/dist/src/neutral/query.d.ts.map +1 -0
  48. package/dist/src/neutral/query.js +180 -0
  49. package/dist/src/neutral/result.d.ts +33 -0
  50. package/dist/src/neutral/result.d.ts.map +1 -0
  51. package/dist/src/neutral/result.js +38 -0
  52. package/dist/src/neutral/source-ref.d.ts +126 -0
  53. package/dist/src/neutral/source-ref.d.ts.map +1 -0
  54. package/dist/src/neutral/source-ref.js +267 -0
  55. package/dist/src/nl-map-control.d.ts.map +1 -1
  56. package/dist/src/tools/count-features.d.ts +85 -11
  57. package/dist/src/tools/count-features.d.ts.map +1 -1
  58. package/dist/src/tools/count-features.js +49 -31
  59. package/dist/src/tools/describe-layer.d.ts +30 -6
  60. package/dist/src/tools/describe-layer.d.ts.map +1 -1
  61. package/dist/src/tools/describe-layer.js +99 -8
  62. package/dist/src/tools/get-extent.d.ts +69 -11
  63. package/dist/src/tools/get-extent.d.ts.map +1 -1
  64. package/dist/src/tools/get-extent.js +28 -46
  65. package/dist/src/tools/list-services.d.ts +7 -0
  66. package/dist/src/tools/list-services.d.ts.map +1 -1
  67. package/dist/src/tools/list-services.js +8 -1
  68. package/dist/src/tools/list-sources.d.ts +44 -0
  69. package/dist/src/tools/list-sources.d.ts.map +1 -0
  70. package/dist/src/tools/list-sources.js +138 -0
  71. package/dist/src/tools/query-features.d.ts +99 -19
  72. package/dist/src/tools/query-features.d.ts.map +1 -1
  73. package/dist/src/tools/query-features.js +38 -45
  74. package/dist/src/tools/statistics.d.ts +80 -12
  75. package/dist/src/tools/statistics.d.ts.map +1 -1
  76. package/dist/src/tools/statistics.js +56 -23
  77. package/package.json +31 -7
@@ -1,19 +1,110 @@
1
1
  import { z } from "zod";
2
2
  import { jsonText } from "../helpers.js";
3
+ import { withCapabilityHonesty } from "../neutral/errors.js";
4
+ import { ogcLayoutFor, resolveSource, sourceRefFields } from "../neutral/source-ref.js";
5
+ /**
6
+ * `honua_describe_layer` — protocol-neutral source description (#1005).
7
+ *
8
+ * The envelope is the same for every protocol: the neutral source reference,
9
+ * its protocol, the capabilities the runtime will actually honor, and a field
10
+ * schema. Where the schema comes from is protocol-specific (GeoServices layer
11
+ * metadata, an OGC collection + its queryables); the shape the agent reads is
12
+ * not. Protocols with no metadata document report `schemaAvailable: false`
13
+ * with a reason instead of an empty field list, which would read as "this
14
+ * source has no attributes".
15
+ */
3
16
  export const schema = z.object({
4
- serviceId: z.string().describe("The feature service ID"),
5
- layerId: z.number().int().nonnegative().describe("The layer ID within the service"),
17
+ ...sourceRefFields,
6
18
  });
7
- export async function execute(client, input) {
8
- const meta = await client.getLayerMetadata(input.serviceId, input.layerId);
9
- return jsonText({
10
- id: meta.id,
11
- name: meta.name,
19
+ async function describeGeoServices(client, resolved) {
20
+ const { serviceId, layerId } = resolved.descriptor.locator;
21
+ const meta = await client.getLayerMetadata(serviceId, layerId);
22
+ return {
23
+ name: meta.name ?? null,
12
24
  description: meta.description ?? null,
13
25
  geometryType: meta.geometryType ?? null,
14
- fields: (meta.fields ?? []).map((f) => ({ name: f.name, type: f.type, alias: f.alias ?? f.name })),
26
+ fields: (meta.fields ?? []).map((f) => ({ name: f.name, type: f.type ?? null, alias: f.alias ?? f.name })),
15
27
  extent: meta.extent ?? null,
16
28
  spatialReference: meta.spatialReference ?? null,
17
29
  relationships: meta.relationships ?? [],
30
+ schemaAvailable: true,
31
+ };
32
+ }
33
+ function bboxToExtent(bbox) {
34
+ if (!bbox || bbox.length < 4)
35
+ return null;
36
+ const [xmin, ymin, xmax, ymax] = bbox;
37
+ return { xmin, ymin, xmax, ymax };
38
+ }
39
+ async function describeOgcCollection(client, resolved) {
40
+ const collectionId = resolved.descriptor.locator.collectionId;
41
+ const layout = await ogcLayoutFor(client, resolved.ref.layout);
42
+ const request = { collectionId, ...(layout ? { layout } : {}) };
43
+ const meta = resolved.descriptor.protocol === "ogc-records"
44
+ ? await client.getOgcRecordCollection(request)
45
+ : await client.getOgcCollection(request);
46
+ let fields = [];
47
+ let schemaAvailable = true;
48
+ let schemaReason;
49
+ try {
50
+ const queryables = await client.getOgcQueryables(request);
51
+ fields = Object.entries(queryables.properties ?? {}).map(([name, property]) => ({
52
+ name,
53
+ type: property.type ?? null,
54
+ alias: property.title ?? name,
55
+ }));
56
+ if (fields.length === 0) {
57
+ schemaAvailable = false;
58
+ schemaReason = "the collection advertises no queryable properties.";
59
+ }
60
+ }
61
+ catch (error) {
62
+ schemaAvailable = false;
63
+ schemaReason = `queryables are not published by this endpoint: ${error instanceof Error ? error.message : String(error)}`;
64
+ }
65
+ const temporal = meta.extent?.temporal?.interval?.[0];
66
+ return {
67
+ name: meta.title ?? meta.id ?? collectionId,
68
+ description: meta.description ?? null,
69
+ geometryType: null,
70
+ fields,
71
+ extent: bboxToExtent(meta.extent?.spatial?.bbox?.[0]),
72
+ spatialReference: meta.extent?.spatial?.crs ?? meta.crs?.[0] ?? null,
73
+ relationships: [],
74
+ ...(temporal ? { timeField: null } : {}),
75
+ schemaAvailable,
76
+ ...(schemaReason ? { schemaReason } : {}),
77
+ };
78
+ }
79
+ function describeFromDescriptorOnly(resolved) {
80
+ return {
81
+ name: resolved.ref.address,
82
+ description: null,
83
+ geometryType: null,
84
+ fields: [],
85
+ extent: null,
86
+ spatialReference: null,
87
+ relationships: [],
88
+ schemaAvailable: false,
89
+ schemaReason: `protocol "${resolved.descriptor.protocol}" publishes no schema document this server can read without a discovery pass; query the source and inspect the returned attributes instead.`,
90
+ };
91
+ }
92
+ export async function execute(client, input) {
93
+ return withCapabilityHonesty(async () => {
94
+ const resolved = resolveSource(client, input);
95
+ const protocol = resolved.descriptor.protocol;
96
+ const envelope = protocol === "geoservices-feature-service" || protocol === "geoservices-map-service" || protocol === "grpc"
97
+ ? await describeGeoServices(client, resolved)
98
+ : protocol === "ogc-features" || protocol === "ogc-records"
99
+ ? await describeOgcCollection(client, resolved)
100
+ : describeFromDescriptorOnly(resolved);
101
+ return jsonText({
102
+ source: resolved.ref.ref,
103
+ protocol,
104
+ capabilities: [...resolved.source.capabilities].sort(),
105
+ ...envelope,
106
+ // Retained for clients written against the pre-#1005 shape.
107
+ id: resolved.descriptor.locator.layerId ?? resolved.ref.address,
108
+ });
18
109
  });
19
110
  }
@@ -1,26 +1,77 @@
1
1
  import type { HonuaClient } from "@honua/sdk-js";
2
2
  import { z } from "zod";
3
+ /**
4
+ * `honua_get_extent` — protocol-neutral bounding box (#1005).
5
+ *
6
+ * Runs the canonical `Source.queryExtent()`. Protocols with no server-side
7
+ * extent operation (OGC API Features, STAC, OData) answer from the collection's
8
+ * declared extent under the degraded capability policy, and say so; protocols
9
+ * that cannot answer at all refuse with a structured capability error.
10
+ */
3
11
  export declare const schema: z.ZodObject<{
4
- serviceId: z.ZodString;
5
- layerId: z.ZodNumber;
12
+ filter: z.ZodOptional<z.ZodType<import("../neutral/filter.js").FilterNode, z.ZodTypeDef, import("../neutral/filter.js").FilterNode>>;
6
13
  where: z.ZodOptional<z.ZodString>;
14
+ bbox: z.ZodOptional<z.ZodArray<z.ZodNumber, "many">>;
15
+ bboxSrid: z.ZodOptional<z.ZodNumber>;
7
16
  geometry: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
8
17
  geometryType: z.ZodOptional<z.ZodEnum<["esriGeometryPoint", "esriGeometryPolyline", "esriGeometryPolygon", "esriGeometryEnvelope", "esriGeometryMultipoint"]>>;
9
- spatialRel: z.ZodOptional<z.ZodEnum<["intersects", "contains", "within"]>>;
18
+ spatialRel: z.ZodOptional<z.ZodEnum<["intersects", "contains", "within", "crosses", "touches", "overlaps", "bbox-intersects"]>>;
19
+ temporal: z.ZodOptional<z.ZodObject<{
20
+ instant: z.ZodOptional<z.ZodString>;
21
+ start: z.ZodOptional<z.ZodNullable<z.ZodString>>;
22
+ end: z.ZodOptional<z.ZodNullable<z.ZodString>>;
23
+ field: z.ZodOptional<z.ZodString>;
24
+ }, "strip", z.ZodTypeAny, {
25
+ field?: string | undefined;
26
+ instant?: string | undefined;
27
+ start?: string | null | undefined;
28
+ end?: string | null | undefined;
29
+ }, {
30
+ field?: string | undefined;
31
+ instant?: string | undefined;
32
+ start?: string | null | undefined;
33
+ end?: string | null | undefined;
34
+ }>>;
35
+ source: z.ZodOptional<z.ZodString>;
36
+ layout: z.ZodOptional<z.ZodEnum<["honua-facade", "ogc-api", "auto", "stac-api", "stac-static"]>>;
37
+ serviceId: z.ZodOptional<z.ZodString>;
38
+ layerId: z.ZodOptional<z.ZodNumber>;
10
39
  }, "strip", z.ZodTypeAny, {
11
- serviceId: string;
12
- layerId: number;
13
- where?: string | undefined;
40
+ filter?: import("../neutral/filter.js").FilterNode | undefined;
41
+ serviceId?: string | undefined;
14
42
  geometry?: Record<string, unknown> | undefined;
43
+ bbox?: number[] | undefined;
15
44
  geometryType?: "esriGeometryPoint" | "esriGeometryPolyline" | "esriGeometryPolygon" | "esriGeometryEnvelope" | "esriGeometryMultipoint" | undefined;
16
- spatialRel?: "intersects" | "contains" | "within" | undefined;
17
- }, {
18
- serviceId: string;
19
- layerId: number;
45
+ temporal?: {
46
+ field?: string | undefined;
47
+ instant?: string | undefined;
48
+ start?: string | null | undefined;
49
+ end?: string | null | undefined;
50
+ } | undefined;
51
+ layout?: "honua-facade" | "ogc-api" | "auto" | "stac-api" | "stac-static" | undefined;
52
+ layerId?: number | undefined;
53
+ source?: string | undefined;
20
54
  where?: string | undefined;
55
+ bboxSrid?: number | undefined;
56
+ spatialRel?: "intersects" | "contains" | "within" | "crosses" | "touches" | "overlaps" | "bbox-intersects" | undefined;
57
+ }, {
58
+ filter?: import("../neutral/filter.js").FilterNode | undefined;
59
+ serviceId?: string | undefined;
21
60
  geometry?: Record<string, unknown> | undefined;
61
+ bbox?: number[] | undefined;
22
62
  geometryType?: "esriGeometryPoint" | "esriGeometryPolyline" | "esriGeometryPolygon" | "esriGeometryEnvelope" | "esriGeometryMultipoint" | undefined;
23
- spatialRel?: "intersects" | "contains" | "within" | undefined;
63
+ temporal?: {
64
+ field?: string | undefined;
65
+ instant?: string | undefined;
66
+ start?: string | null | undefined;
67
+ end?: string | null | undefined;
68
+ } | undefined;
69
+ layout?: "honua-facade" | "ogc-api" | "auto" | "stac-api" | "stac-static" | undefined;
70
+ layerId?: number | undefined;
71
+ source?: string | undefined;
72
+ where?: string | undefined;
73
+ bboxSrid?: number | undefined;
74
+ spatialRel?: "intersects" | "contains" | "within" | "crosses" | "touches" | "overlaps" | "bbox-intersects" | undefined;
24
75
  }>;
25
76
  export type Input = z.infer<typeof schema>;
26
77
  export declare function execute(client: HonuaClient, input: Input): Promise<{
@@ -28,5 +79,12 @@ export declare function execute(client: HonuaClient, input: Input): Promise<{
28
79
  type: "text";
29
80
  text: string;
30
81
  }>;
82
+ } | {
83
+ structuredContent: Record<string, unknown>;
84
+ isError: true;
85
+ content: Array<{
86
+ type: "text";
87
+ text: string;
88
+ }>;
31
89
  }>;
32
90
  //# sourceMappingURL=get-extent.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"get-extent.d.ts","sourceRoot":"","sources":["../../../src/tools/get-extent.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AACjD,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,eAAO,MAAM,MAAM;;;;;;;;;;;;;;;;;;;;;EAajB,CAAC;AAEH,MAAM,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,MAAM,CAAC,CAAC;AAE3C,wBAAsB,OAAO,CAAC,MAAM,EAAE,WAAW,EAAE,KAAK,EAAE,KAAK;;;;;GAoC9D"}
1
+ {"version":3,"file":"get-extent.d.ts","sourceRoot":"","sources":["../../../src/tools/get-extent.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AACjD,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAMxB;;;;;;;GAOG;AACH,eAAO,MAAM,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAGjB,CAAC;AAEH,MAAM,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,MAAM,CAAC,CAAC;AAE3C,wBAAsB,OAAO,CAAC,MAAM,EAAE,WAAW,EAAE,KAAK,EAAE,KAAK;;;;;;;;;;;;GAgB9D"}
@@ -1,51 +1,33 @@
1
1
  import { z } from "zod";
2
- import { GEOMETRY_TYPES, coerceCount, jsonText, mapSpatialRel, resolveGeometryType } from "../helpers.js";
2
+ import { jsonText } from "../helpers.js";
3
+ import { withCapabilityHonesty } from "../neutral/errors.js";
4
+ import { queryFilterFields, toQuery } from "../neutral/query.js";
5
+ import { resolveSource, sourceRefFields } from "../neutral/source-ref.js";
6
+ /**
7
+ * `honua_get_extent` — protocol-neutral bounding box (#1005).
8
+ *
9
+ * Runs the canonical `Source.queryExtent()`. Protocols with no server-side
10
+ * extent operation (OGC API Features, STAC, OData) answer from the collection's
11
+ * declared extent under the degraded capability policy, and say so; protocols
12
+ * that cannot answer at all refuse with a structured capability error.
13
+ */
3
14
  export const schema = z.object({
4
- serviceId: z.string().describe("The feature service ID"),
5
- layerId: z.number().int().nonnegative().describe("The layer ID within the service"),
6
- where: z.string().optional().describe("SQL WHERE clause"),
7
- geometry: z.record(z.unknown()).optional().describe("Esri JSON geometry for spatial filter"),
8
- geometryType: z
9
- .enum(GEOMETRY_TYPES)
10
- .optional()
11
- .describe("Esri geometry type. If omitted, inferred from geometry when possible."),
12
- spatialRel: z
13
- .enum(["intersects", "contains", "within"])
14
- .optional()
15
- .describe("Spatial relationship (default: intersects)"),
15
+ ...sourceRefFields,
16
+ ...queryFilterFields,
16
17
  });
17
18
  export async function execute(client, input) {
18
- const response = (await client.queryFeatures({
19
- serviceId: input.serviceId,
20
- layerId: input.layerId,
21
- where: input.where,
22
- geometry: input.geometry,
23
- geometryType: resolveGeometryType(input.geometry, input.geometryType),
24
- spatialRel: mapSpatialRel(input.spatialRel),
25
- returnGeometry: false,
26
- extraParams: { returnExtentOnly: true },
27
- }));
28
- // Accept large counts returned as strings by the grpc-web transport (chosen
29
- // to preserve precision beyond Number.MAX_SAFE_INTEGER) as well as numbers.
30
- const count = coerceCount(response.count);
31
- const hasExtent = Object.prototype.hasOwnProperty.call(response, "extent");
32
- if (!hasExtent) {
33
- // Some backends can return count-only payloads for empty extent queries.
34
- if (count !== undefined) {
35
- return jsonText({ extent: null, count });
36
- }
37
- throw new Error("Extent query did not return an extent payload.");
38
- }
39
- let extent;
40
- if (response.extent === null) {
41
- extent = null;
42
- }
43
- else if (typeof response.extent === "object" && !Array.isArray(response.extent)) {
44
- // A JSON array is `typeof "object"` but is not a valid extent envelope.
45
- extent = response.extent;
46
- }
47
- else {
48
- throw new Error("Extent query returned an invalid extent payload.");
49
- }
50
- return jsonText({ extent, count });
19
+ return withCapabilityHonesty(async () => {
20
+ const resolved = resolveSource(client, input);
21
+ const query = toQuery(input, { protocol: resolved.descriptor.protocol, paginate: false });
22
+ const { extent, count } = await resolved.source.queryExtent(query);
23
+ return jsonText({
24
+ source: resolved.ref.ref,
25
+ protocol: resolved.descriptor.protocol,
26
+ extent: extent ?? null,
27
+ count: count ?? null,
28
+ // An extent that did not come from a server-side extent operation is an
29
+ // honest, but weaker, answer; name the capability so the caller can tell.
30
+ extentCapability: resolved.source.capabilities.has("queryExtent") ? "server" : "declared-extent",
31
+ });
32
+ });
51
33
  }
@@ -1,5 +1,12 @@
1
1
  import type { HonuaClient } from "@honua/sdk-js";
2
2
  import { z } from "zod";
3
+ /**
4
+ * `honua_list_services` — the GeoServices-specific service catalog.
5
+ *
6
+ * Retained for existing clients; `honua_list_sources` is the protocol-neutral
7
+ * replacement (#1005), because "what services are under /rest/services?" is a
8
+ * question only an Esri endpoint can answer.
9
+ */
3
10
  export declare const schema: z.ZodObject<{
4
11
  includeDetails: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
5
12
  }, "strip", z.ZodTypeAny, {
@@ -1 +1 @@
1
- {"version":3,"file":"list-services.d.ts","sourceRoot":"","sources":["../../../src/tools/list-services.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AACjD,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,eAAO,MAAM,MAAM;;;;;;EAQjB,CAAC;AAEH,MAAM,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,MAAM,CAAC,CAAC;AAI3C,wBAAsB,OAAO,CAAC,MAAM,EAAE,WAAW,EAAE,KAAK,EAAE,KAAK;;;;;GA+C9D"}
1
+ {"version":3,"file":"list-services.d.ts","sourceRoot":"","sources":["../../../src/tools/list-services.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AACjD,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB;;;;;;GAMG;AACH,eAAO,MAAM,MAAM;;;;;;EAQjB,CAAC;AAEH,MAAM,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,MAAM,CAAC,CAAC;AAI3C,wBAAsB,OAAO,CAAC,MAAM,EAAE,WAAW,EAAE,KAAK,EAAE,KAAK;;;;;GA+C9D"}
@@ -1,5 +1,12 @@
1
1
  import { z } from "zod";
2
2
  import { jsonText, mapWithConcurrency, metadataErrorText } from "../helpers.js";
3
+ /**
4
+ * `honua_list_services` — the GeoServices-specific service catalog.
5
+ *
6
+ * Retained for existing clients; `honua_list_sources` is the protocol-neutral
7
+ * replacement (#1005), because "what services are under /rest/services?" is a
8
+ * question only an Esri endpoint can answer.
9
+ */
3
10
  export const schema = z.object({
4
11
  includeDetails: z
5
12
  .boolean()
@@ -22,7 +29,7 @@ export async function execute(client, input) {
22
29
  catalogAvailable: false,
23
30
  services: [],
24
31
  reason: `service catalog is not available on this target: ${err instanceof Error ? err.message : String(err)}`,
25
- guidance: "No /rest/services directory here. Call honua_describe_layer / honua_query_features with a known serviceId and layerId, or point the base URL at an ArcGIS folder that lists services.",
32
+ guidance: "No /rest/services directory here — this tool only sees a GeoServices catalog. Call honua_list_sources, which probes every protocol family this endpoint may publish (OGC API Features included) and returns protocol-neutral source references.",
26
33
  });
27
34
  }
28
35
  const services = (response.services ?? []).filter((s) => s.type === "FeatureServer");
@@ -0,0 +1,44 @@
1
+ import type { HonuaClient } from "@honua/sdk-js";
2
+ import { z } from "zod";
3
+ /**
4
+ * `honua_list_sources` — protocol-neutral discovery (#1005).
5
+ *
6
+ * `honua_list_services` answers a GeoServices-only question ("what is in
7
+ * `/rest/services`?"). This tool answers the neutral one — "what sources can I
8
+ * query here, and how do I address them?" — and emits the exact
9
+ * `<protocol>:<address>` references every other tool accepts, so an agent never
10
+ * has to construct one.
11
+ *
12
+ * Each protocol family is probed independently and degrades on its own: an
13
+ * endpoint that serves OGC collections but has no `/rest/services` directory
14
+ * reports the GeoServices family as unavailable-with-reason and still returns
15
+ * its OGC sources. A family that is absent is never reported as "no sources".
16
+ */
17
+ export declare const schema: z.ZodObject<{
18
+ protocol: z.ZodDefault<z.ZodOptional<z.ZodEnum<["auto", "geoservices", "ogc-features", "ogc-records"]>>>;
19
+ layout: z.ZodOptional<z.ZodEnum<["honua-facade", "ogc-api", "auto", "stac-api", "stac-static"]>>;
20
+ maxServices: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
21
+ }, "strip", z.ZodTypeAny, {
22
+ protocol: "ogc-features" | "ogc-records" | "geoservices" | "auto";
23
+ maxServices: number;
24
+ layout?: "honua-facade" | "ogc-api" | "auto" | "stac-api" | "stac-static" | undefined;
25
+ }, {
26
+ layout?: "honua-facade" | "ogc-api" | "auto" | "stac-api" | "stac-static" | undefined;
27
+ protocol?: "ogc-features" | "ogc-records" | "geoservices" | "auto" | undefined;
28
+ maxServices?: number | undefined;
29
+ }>;
30
+ export type Input = z.infer<typeof schema>;
31
+ export declare function execute(client: HonuaClient, input: Input): Promise<{
32
+ content: Array<{
33
+ type: "text";
34
+ text: string;
35
+ }>;
36
+ } | {
37
+ structuredContent: Record<string, unknown>;
38
+ isError: true;
39
+ content: Array<{
40
+ type: "text";
41
+ text: string;
42
+ }>;
43
+ }>;
44
+ //# sourceMappingURL=list-sources.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"list-sources.d.ts","sourceRoot":"","sources":["../../../src/tools/list-sources.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAEjD,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAKxB;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,MAAM;;;;;;;;;;;;EAoBjB,CAAC;AAEH,MAAM,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,MAAM,CAAC,CAAC;AA6F3C,wBAAsB,OAAO,CAAC,MAAM,EAAE,WAAW,EAAE,KAAK,EAAE,KAAK;;;;;;;;;;;;GAkC9D"}
@@ -0,0 +1,138 @@
1
+ import { PROTOCOL_DEFAULT_CAPABILITIES } from "@honua/sdk-js/contract";
2
+ import { z } from "zod";
3
+ import { jsonText, mapWithConcurrency } from "../helpers.js";
4
+ import { withCapabilityHonesty } from "../neutral/errors.js";
5
+ import { SOURCE_LAYOUTS, ogcLayoutFor } from "../neutral/source-ref.js";
6
+ /**
7
+ * `honua_list_sources` — protocol-neutral discovery (#1005).
8
+ *
9
+ * `honua_list_services` answers a GeoServices-only question ("what is in
10
+ * `/rest/services`?"). This tool answers the neutral one — "what sources can I
11
+ * query here, and how do I address them?" — and emits the exact
12
+ * `<protocol>:<address>` references every other tool accepts, so an agent never
13
+ * has to construct one.
14
+ *
15
+ * Each protocol family is probed independently and degrades on its own: an
16
+ * endpoint that serves OGC collections but has no `/rest/services` directory
17
+ * reports the GeoServices family as unavailable-with-reason and still returns
18
+ * its OGC sources. A family that is absent is never reported as "no sources".
19
+ */
20
+ export const schema = z.object({
21
+ protocol: z
22
+ .enum(["auto", "geoservices", "ogc-features", "ogc-records"])
23
+ .optional()
24
+ .default("auto")
25
+ .describe("Restrict discovery to one protocol family. `auto` (default) probes every family this server can list."),
26
+ layout: z
27
+ .enum(SOURCE_LAYOUTS)
28
+ .optional()
29
+ .describe('Endpoint layout for the OGC families: "honua-facade" (default) or "ogc-api"/"auto" for a third-party OGC API root.'),
30
+ maxServices: z
31
+ .number()
32
+ .int()
33
+ .positive()
34
+ .max(200)
35
+ .optional()
36
+ .default(25)
37
+ .describe("Maximum GeoServices services expanded into per-layer sources (each costs one metadata request)."),
38
+ });
39
+ const METADATA_CONCURRENCY = 8;
40
+ function capabilitiesFor(protocol) {
41
+ return [...PROTOCOL_DEFAULT_CAPABILITIES[protocol]].sort();
42
+ }
43
+ async function discoverGeoServices(client, maxServices) {
44
+ let response;
45
+ try {
46
+ response = await client.listServices();
47
+ }
48
+ catch (err) {
49
+ return {
50
+ available: false,
51
+ reason: `no GeoServices catalog on this target: ${err instanceof Error ? err.message : String(err)}`,
52
+ sources: [],
53
+ };
54
+ }
55
+ const services = (response.services ?? []).filter((s) => s.type === "FeatureServer" || s.type === "MapServer");
56
+ const expanded = services.slice(0, maxServices);
57
+ const perService = await mapWithConcurrency(expanded, METADATA_CONCURRENCY, async (service) => {
58
+ const protocol = service.type === "MapServer" ? "geoservices-map-service" : "geoservices-feature-service";
59
+ try {
60
+ const meta = await client.getFeatureServiceMetadata(service.name);
61
+ const layers = [...(meta.layers ?? []), ...(meta.tables ?? [])];
62
+ return layers.map((layer) => ({
63
+ source: `${protocol}:${service.name}/${layer.id}`,
64
+ protocol,
65
+ title: layer.name ?? null,
66
+ capabilities: capabilitiesFor(protocol),
67
+ }));
68
+ }
69
+ catch {
70
+ // The service exists but its metadata is unreadable; still emit the
71
+ // conventional layer-0 reference rather than dropping the service.
72
+ return [
73
+ {
74
+ source: `${protocol}:${service.name}/0`,
75
+ protocol,
76
+ title: service.name,
77
+ capabilities: capabilitiesFor(protocol),
78
+ },
79
+ ];
80
+ }
81
+ });
82
+ return { available: true, sources: perService.flat() };
83
+ }
84
+ async function discoverOgc(client, family, layout) {
85
+ try {
86
+ const resolved = await ogcLayoutFor(client, layout);
87
+ const request = resolved ? { layout: resolved } : {};
88
+ const response = family === "ogc-records"
89
+ ? await client.listOgcRecordCollections(request)
90
+ : await client.listOgcCollections(request);
91
+ return {
92
+ available: true,
93
+ sources: (response.collections ?? []).map((collection) => ({
94
+ source: `${family}:${collection.id}`,
95
+ protocol: family,
96
+ title: collection.title ?? collection.id,
97
+ capabilities: capabilitiesFor(family),
98
+ })),
99
+ };
100
+ }
101
+ catch (err) {
102
+ return {
103
+ available: false,
104
+ reason: `no ${family} collections on this target: ${err instanceof Error ? err.message : String(err)}`,
105
+ sources: [],
106
+ };
107
+ }
108
+ }
109
+ export async function execute(client, input) {
110
+ return withCapabilityHonesty(async () => {
111
+ const families = {};
112
+ if (input.protocol === "auto" || input.protocol === "geoservices") {
113
+ families.geoservices = await discoverGeoServices(client, input.maxServices);
114
+ }
115
+ if (input.protocol === "auto" || input.protocol === "ogc-features") {
116
+ families["ogc-features"] = await discoverOgc(client, "ogc-features", input.layout);
117
+ }
118
+ if (input.protocol === "ogc-records") {
119
+ families["ogc-records"] = await discoverOgc(client, "ogc-records", input.layout);
120
+ }
121
+ const sources = Object.values(families).flatMap((family) => family.sources);
122
+ return jsonText({
123
+ sources,
124
+ sourceCount: sources.length,
125
+ families: Object.fromEntries(Object.entries(families).map(([name, outcome]) => [
126
+ name,
127
+ {
128
+ available: outcome.available,
129
+ ...(outcome.reason ? { reason: outcome.reason } : {}),
130
+ count: outcome.sources.length,
131
+ },
132
+ ])),
133
+ guidance: sources.length > 0
134
+ ? "Pass a `source` value verbatim to honua_describe_layer / honua_query_features / honua_count_features / honua_get_extent / honua_statistics."
135
+ : 'No source catalog is published here. Address a known source directly, e.g. source="ogc-features:<collectionId>", "stac:<collectionId>", "wfs:<typeName>", "odata:<entitySet>", or "geoservices-feature-service:<serviceId>/<layerId>".',
136
+ });
137
+ });
138
+ }