@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
@@ -0,0 +1,267 @@
1
+ import { HonuaCapabilityNotSupportedError } from "@honua/sdk-js";
2
+ import { PROTOCOL_DEFAULT_CAPABILITIES, createDataset } from "@honua/sdk-js/contract";
3
+ import { z } from "zod";
4
+ /**
5
+ * PROTOCOL-NEUTRAL source addressing for the MCP tool contract (issue #1005).
6
+ *
7
+ * The tool surface used to address data the way GeoServices does — a required
8
+ * `serviceId` string plus a required numeric `layerId`. That vocabulary is
9
+ * meaningless on OGC API Features, STAC, WFS, and OData, so a "vendor-neutral"
10
+ * server whose only addressing mode is Esri-shaped concedes the claim on
11
+ * inspection.
12
+ *
13
+ * A source is now addressed the way the SDK addresses one: a
14
+ * `SourceDescriptor` (protocol + locator) resolved into a canonical
15
+ * `Source` through `createDataset`. Over the wire that descriptor is carried as
16
+ * one compact string:
17
+ *
18
+ * ```text
19
+ * <protocol>:<address>
20
+ * ```
21
+ *
22
+ * | protocol | address form | example |
23
+ * | ------------------------------- | --------------------- | -------------------------------------- |
24
+ * | `geoservices-feature-service` | `<serviceId>/<layer>` | `geoservices-feature-service:Parks/0` |
25
+ * | `geoservices-map-service` | `<serviceId>/<layer>` | `geoservices-map-service:Basemap/2` |
26
+ * | `ogc-features` | `<collectionId>` | `ogc-features:hotels` |
27
+ * | `ogc-records` | `<collectionId>` | `ogc-records:catalog` |
28
+ * | `stac` | `<collectionId>` | `stac:sentinel-2-l2a` |
29
+ * | `wfs` | `<typeName>` | `wfs:topp:states` |
30
+ * | `odata` | `<entitySet>` | `odata:People` |
31
+ *
32
+ * `honua_list_sources` emits exactly these strings, so the normal agent flow is
33
+ * discover-then-address with no parsing on the client side. The reference is
34
+ * deliberately fail-closed: an unrecognized protocol token is refused with the
35
+ * accepted forms rather than guessed at, because guessing "which protocol did
36
+ * they mean" is how a neutral surface silently becomes a single-vendor one.
37
+ *
38
+ * The legacy `serviceId` + `layerId` pair is still accepted (see
39
+ * {@link legacyGeoServicesRef}) but is deprecated and is no longer *required* by
40
+ * any schema.
41
+ */
42
+ /** Protocols reachable through a `<protocol>:<address>` source reference. */
43
+ export const SOURCE_REF_PROTOCOLS = [
44
+ "geoservices-feature-service",
45
+ "geoservices-map-service",
46
+ "ogc-features",
47
+ "ogc-records",
48
+ "stac",
49
+ "wfs",
50
+ "odata",
51
+ ];
52
+ /** Short, human-friendly aliases accepted in place of the canonical token. */
53
+ const PROTOCOL_ALIASES = {
54
+ geoservices: "geoservices-feature-service",
55
+ "feature-service": "geoservices-feature-service",
56
+ featureserver: "geoservices-feature-service",
57
+ "map-service": "geoservices-map-service",
58
+ mapserver: "geoservices-map-service",
59
+ ogc: "ogc-features",
60
+ "ogc-api-features": "ogc-features",
61
+ features: "ogc-features",
62
+ records: "ogc-records",
63
+ };
64
+ /** Endpoint layouts an OGC API / STAC source may be published under. */
65
+ export const SOURCE_LAYOUTS = ["honua-facade", "ogc-api", "auto", "stac-api", "stac-static"];
66
+ export class SourceRefError extends Error {
67
+ code = "INVALID_SOURCE_REFERENCE";
68
+ constructor(message) {
69
+ super(message);
70
+ this.name = "SourceRefError";
71
+ }
72
+ }
73
+ const ACCEPTED_FORMS = SOURCE_REF_PROTOCOLS.map((protocol) => {
74
+ switch (protocol) {
75
+ case "geoservices-feature-service":
76
+ case "geoservices-map-service":
77
+ return `${protocol}:<serviceId>/<layerId>`;
78
+ case "wfs":
79
+ return `${protocol}:<typeName>`;
80
+ case "odata":
81
+ return `${protocol}:<entitySet>`;
82
+ default:
83
+ return `${protocol}:<collectionId>`;
84
+ }
85
+ }).join(", ");
86
+ function normalizeProtocolToken(token) {
87
+ const normalized = token.trim().toLowerCase();
88
+ if (SOURCE_REF_PROTOCOLS.includes(normalized)) {
89
+ return normalized;
90
+ }
91
+ return PROTOCOL_ALIASES[normalized];
92
+ }
93
+ /**
94
+ * Parse a `<protocol>:<address>` source reference. Fails closed: an omitted or
95
+ * unrecognized protocol token is refused with the accepted forms instead of
96
+ * being inferred, so a mistyped reference can never silently resolve against
97
+ * the wrong protocol.
98
+ */
99
+ export function parseSourceRef(raw, layout) {
100
+ const value = raw.trim();
101
+ if (value.length === 0) {
102
+ throw new SourceRefError(`source must be a non-empty "<protocol>:<address>" reference. Accepted: ${ACCEPTED_FORMS}`);
103
+ }
104
+ const separator = value.indexOf(":");
105
+ if (separator < 0) {
106
+ throw new SourceRefError(`source "${raw}" is missing its protocol prefix. Use "<protocol>:<address>" (accepted: ${ACCEPTED_FORMS}), or call honua_list_sources to discover exact source references.`);
107
+ }
108
+ const protocol = normalizeProtocolToken(value.slice(0, separator));
109
+ if (!protocol) {
110
+ throw new SourceRefError(`source "${raw}" names an unknown protocol "${value.slice(0, separator)}". Accepted: ${ACCEPTED_FORMS}.`);
111
+ }
112
+ // The address keeps every remaining colon: a WFS typeName is namespace-qualified
113
+ // (`topp:states`) and splitting it again would corrupt the identifier.
114
+ const address = value.slice(separator + 1).trim();
115
+ if (address.length === 0) {
116
+ throw new SourceRefError(`source "${raw}" is missing its address. Accepted: ${ACCEPTED_FORMS}.`);
117
+ }
118
+ return { ref: `${protocol}:${address}`, protocol, address, ...(layout ? { layout } : {}) };
119
+ }
120
+ /** Build the canonical reference for the deprecated GeoServices `serviceId`/`layerId` pair. */
121
+ export function legacyGeoServicesRef(serviceId, layerId) {
122
+ return `geoservices-feature-service:${serviceId}/${layerId}`;
123
+ }
124
+ function splitGeoServicesAddress(ref) {
125
+ const slash = ref.address.lastIndexOf("/");
126
+ if (slash <= 0 || slash === ref.address.length - 1) {
127
+ throw new SourceRefError(`source "${ref.ref}" must address a layer as "<serviceId>/<layerId>" (e.g. "${ref.protocol}:Parks/0").`);
128
+ }
129
+ const serviceId = ref.address.slice(0, slash);
130
+ const rawLayer = ref.address.slice(slash + 1);
131
+ if (!/^\d+$/.test(rawLayer)) {
132
+ throw new SourceRefError(`source "${ref.ref}" has a non-numeric layer id "${rawLayer}"; expected a whole number.`);
133
+ }
134
+ return { serviceId, layerId: Number.parseInt(rawLayer, 10) };
135
+ }
136
+ /** Project a parsed reference onto the SDK's protocol-neutral `SourceLocator`. */
137
+ export function toSourceLocator(ref, baseUrl) {
138
+ const layout = ref.layout && ref.layout !== "honua-facade" ? { layout: ref.layout } : {};
139
+ switch (ref.protocol) {
140
+ case "geoservices-feature-service":
141
+ case "geoservices-map-service": {
142
+ const { serviceId, layerId } = splitGeoServicesAddress(ref);
143
+ return { url: baseUrl, serviceId, layerId };
144
+ }
145
+ case "ogc-features":
146
+ case "ogc-records":
147
+ case "stac":
148
+ return { url: baseUrl, collectionId: ref.address, ...layout };
149
+ case "wfs":
150
+ return { url: baseUrl, typeName: ref.address };
151
+ case "odata":
152
+ return { url: baseUrl, entitySet: ref.address };
153
+ }
154
+ }
155
+ /** Build the `SourceDescriptor` a reference denotes, without contacting the endpoint. */
156
+ export function toSourceDescriptor(ref, baseUrl) {
157
+ return {
158
+ id: ref.ref,
159
+ protocol: ref.protocol,
160
+ locator: toSourceLocator(ref, baseUrl),
161
+ capabilities: PROTOCOL_DEFAULT_CAPABILITIES[ref.protocol],
162
+ };
163
+ }
164
+ /**
165
+ * Zod fields every source-addressed tool mixes into its input schema.
166
+ *
167
+ * `source` is the protocol-neutral reference; `serviceId`/`layerId` remain as
168
+ * OPTIONAL, deprecated compatibility inputs so existing MCP clients keep
169
+ * working. No Esri-only field is required by any tool schema.
170
+ */
171
+ export const sourceRefFields = {
172
+ source: z
173
+ .string()
174
+ .optional()
175
+ .describe('Protocol-neutral source reference "<protocol>:<address>" as emitted by honua_list_sources — e.g. "ogc-features:hotels", "stac:sentinel-2-l2a", "wfs:topp:states", "odata:People", "geoservices-feature-service:Parks/0". Preferred over the deprecated serviceId/layerId pair.'),
176
+ layout: z
177
+ .enum(SOURCE_LAYOUTS)
178
+ .optional()
179
+ .describe('Endpoint layout for OGC API / STAC sources: "honua-facade" (default, Honua server paths), "ogc-api"/"auto" (third-party OGC API root), "stac-api"/"stac-static" (STAC). Ignored by other protocols.'),
180
+ serviceId: z
181
+ .string()
182
+ .optional()
183
+ .describe("[DEPRECATED — use `source`] GeoServices service id. Only meaningful on an Esri FeatureServer/MapServer."),
184
+ layerId: z
185
+ .number()
186
+ .int()
187
+ .nonnegative()
188
+ .optional()
189
+ .describe("[DEPRECATED — use `source`] GeoServices layer id within the service. Required with `serviceId`."),
190
+ };
191
+ /** Resolve the neutral reference an input addresses (without building a `Source`). */
192
+ export function resolveSourceRef(input) {
193
+ if (input.source !== undefined && input.source.trim().length > 0) {
194
+ return { ref: parseSourceRef(input.source, input.layout), legacyAddressing: false };
195
+ }
196
+ if (input.serviceId !== undefined && input.serviceId.trim().length > 0) {
197
+ if (input.layerId === undefined) {
198
+ throw new SourceRefError(`serviceId "${input.serviceId}" was given without layerId. Pass both, or (preferred) a protocol-neutral \`source\` reference such as "geoservices-feature-service:${input.serviceId}/0".`);
199
+ }
200
+ return {
201
+ ref: parseSourceRef(legacyGeoServicesRef(input.serviceId, input.layerId), input.layout),
202
+ legacyAddressing: true,
203
+ };
204
+ }
205
+ if (input.layerId !== undefined) {
206
+ throw new SourceRefError("layerId was given without serviceId. Pass both, or (preferred) a protocol-neutral `source` reference.");
207
+ }
208
+ throw new SourceRefError(`no source was addressed. Pass \`source\` as "<protocol>:<address>" (accepted: ${ACCEPTED_FORMS}); call honua_list_sources to discover the exact references this endpoint serves.`);
209
+ }
210
+ /**
211
+ * Resolve an input onto a canonical `Source`.
212
+ *
213
+ * The dataset runs under the `degraded` capability policy so a protocol that
214
+ * cannot serve a request first-party (OGC API Features has no server-side
215
+ * aggregation, for example) answers with an explicit `Result.degraded` reason
216
+ * instead of failing outright. Capabilities the protocol cannot serve at all
217
+ * still throw `HonuaCapabilityNotSupportedError`, which the tool layer turns
218
+ * into a structured tool error — never a silent empty result.
219
+ */
220
+ export function resolveSource(client, input) {
221
+ const { ref, legacyAddressing } = resolveSourceRef(input);
222
+ const descriptor = toSourceDescriptor(ref, client.serverBaseUrl);
223
+ const dataset = createDataset({
224
+ id: `honua-mcp:${ref.ref}`,
225
+ client,
226
+ sources: [descriptor],
227
+ capabilityPolicy: "degraded",
228
+ // The standalone server points at arbitrary public endpoints; a Honua
229
+ // server-version handshake is neither available nor meaningful there.
230
+ skipCompatibilityCheck: true,
231
+ });
232
+ let source;
233
+ try {
234
+ source = dataset.source(descriptor.id);
235
+ }
236
+ catch (error) {
237
+ // A capability refusal is the SDK's own honest signal and travels as-is.
238
+ // Anything else (an incomplete locator, a runtime that cannot construct the
239
+ // protocol adapter) becomes an actionable addressing error rather than an
240
+ // opaque stack trace.
241
+ if (error instanceof HonuaCapabilityNotSupportedError) {
242
+ throw error;
243
+ }
244
+ throw new SourceRefError(`source "${ref.ref}" could not be resolved to a runtime adapter: ${error instanceof Error ? error.message : String(error)}`);
245
+ }
246
+ if (!source) {
247
+ throw new SourceRefError(`source "${ref.ref}" could not be resolved to a runtime adapter.`);
248
+ }
249
+ return { ref, descriptor, source, legacyAddressing };
250
+ }
251
+ /** True when a resolved source speaks a GeoServices (Esri) protocol. */
252
+ export function isGeoServicesProtocol(protocol) {
253
+ return protocol.startsWith("geoservices-") || protocol === "grpc";
254
+ }
255
+ /**
256
+ * Resolve the OGC API Features endpoint layout a metadata call must use.
257
+ *
258
+ * `honua-facade` (the default) needs no resolution; a third-party OGC API root
259
+ * is discovered from its landing page once and cached on the client. STAC
260
+ * layouts are not OGC API Features layouts and resolve to `undefined`.
261
+ */
262
+ export function ogcLayoutFor(client, layout) {
263
+ if (layout === "ogc-api" || layout === "auto") {
264
+ return client.resolveOgcFeaturesLayout(layout);
265
+ }
266
+ return Promise.resolve(undefined);
267
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"nl-map-control.d.ts","sourceRoot":"","sources":["../../src/nl-map-control.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"nl-map-control.d.ts","sourceRoot":"","sources":["../../src/nl-map-control.ts"],"names":[],"mappings":"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"}
@@ -1,32 +1,106 @@
1
1
  import type { HonuaClient } from "@honua/sdk-js";
2
+ import type { CapabilityAwareSource, Query } from "@honua/sdk-js/contract";
2
3
  import { z } from "zod";
4
+ import { projectDegraded } from "../neutral/result.js";
5
+ /**
6
+ * `honua_count_features` — protocol-neutral cardinality check (#1005).
7
+ *
8
+ * There is no single "count" verb across the protocols, so the count is taken
9
+ * from whichever canonical path the source's declared capabilities support:
10
+ *
11
+ * 1. `queryExtent` (GeoServices, WFS, gRPC) returns an exact server count
12
+ * alongside the envelope in one round trip.
13
+ * 2. otherwise a single-record `query` reports `totalCount`, which is where
14
+ * OGC API Features / STAC / OData carry `numberMatched`.
15
+ *
16
+ * When neither path yields a number the tool refuses with a structured
17
+ * capability error rather than reporting `0`.
18
+ */
3
19
  export declare const schema: z.ZodObject<{
4
- serviceId: z.ZodString;
5
- layerId: z.ZodNumber;
20
+ filter: z.ZodOptional<z.ZodType<import("../neutral/filter.js").FilterNode, z.ZodTypeDef, import("../neutral/filter.js").FilterNode>>;
6
21
  where: z.ZodOptional<z.ZodString>;
22
+ bbox: z.ZodOptional<z.ZodArray<z.ZodNumber, "many">>;
23
+ bboxSrid: z.ZodOptional<z.ZodNumber>;
7
24
  geometry: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
8
25
  geometryType: z.ZodOptional<z.ZodEnum<["esriGeometryPoint", "esriGeometryPolyline", "esriGeometryPolygon", "esriGeometryEnvelope", "esriGeometryMultipoint"]>>;
9
- spatialRel: z.ZodOptional<z.ZodEnum<["intersects", "contains", "within"]>>;
26
+ spatialRel: z.ZodOptional<z.ZodEnum<["intersects", "contains", "within", "crosses", "touches", "overlaps", "bbox-intersects"]>>;
27
+ temporal: z.ZodOptional<z.ZodObject<{
28
+ instant: z.ZodOptional<z.ZodString>;
29
+ start: z.ZodOptional<z.ZodNullable<z.ZodString>>;
30
+ end: z.ZodOptional<z.ZodNullable<z.ZodString>>;
31
+ field: z.ZodOptional<z.ZodString>;
32
+ }, "strip", z.ZodTypeAny, {
33
+ field?: string | undefined;
34
+ instant?: string | undefined;
35
+ start?: string | null | undefined;
36
+ end?: string | null | undefined;
37
+ }, {
38
+ field?: string | undefined;
39
+ instant?: string | undefined;
40
+ start?: string | null | undefined;
41
+ end?: string | null | undefined;
42
+ }>>;
43
+ source: z.ZodOptional<z.ZodString>;
44
+ layout: z.ZodOptional<z.ZodEnum<["honua-facade", "ogc-api", "auto", "stac-api", "stac-static"]>>;
45
+ serviceId: z.ZodOptional<z.ZodString>;
46
+ layerId: z.ZodOptional<z.ZodNumber>;
10
47
  }, "strip", z.ZodTypeAny, {
11
- serviceId: string;
12
- layerId: number;
13
- where?: string | undefined;
48
+ filter?: import("../neutral/filter.js").FilterNode | undefined;
49
+ serviceId?: string | undefined;
14
50
  geometry?: Record<string, unknown> | undefined;
51
+ bbox?: number[] | undefined;
15
52
  geometryType?: "esriGeometryPoint" | "esriGeometryPolyline" | "esriGeometryPolygon" | "esriGeometryEnvelope" | "esriGeometryMultipoint" | undefined;
16
- spatialRel?: "intersects" | "contains" | "within" | undefined;
17
- }, {
18
- serviceId: string;
19
- layerId: number;
53
+ temporal?: {
54
+ field?: string | undefined;
55
+ instant?: string | undefined;
56
+ start?: string | null | undefined;
57
+ end?: string | null | undefined;
58
+ } | undefined;
59
+ layout?: "honua-facade" | "ogc-api" | "auto" | "stac-api" | "stac-static" | undefined;
60
+ layerId?: number | undefined;
61
+ source?: string | undefined;
20
62
  where?: string | undefined;
63
+ bboxSrid?: number | undefined;
64
+ spatialRel?: "intersects" | "contains" | "within" | "crosses" | "touches" | "overlaps" | "bbox-intersects" | undefined;
65
+ }, {
66
+ filter?: import("../neutral/filter.js").FilterNode | undefined;
67
+ serviceId?: string | undefined;
21
68
  geometry?: Record<string, unknown> | undefined;
69
+ bbox?: number[] | undefined;
22
70
  geometryType?: "esriGeometryPoint" | "esriGeometryPolyline" | "esriGeometryPolygon" | "esriGeometryEnvelope" | "esriGeometryMultipoint" | undefined;
23
- spatialRel?: "intersects" | "contains" | "within" | undefined;
71
+ temporal?: {
72
+ field?: string | undefined;
73
+ instant?: string | undefined;
74
+ start?: string | null | undefined;
75
+ end?: string | null | undefined;
76
+ } | undefined;
77
+ layout?: "honua-facade" | "ogc-api" | "auto" | "stac-api" | "stac-static" | undefined;
78
+ layerId?: number | undefined;
79
+ source?: string | undefined;
80
+ where?: string | undefined;
81
+ bboxSrid?: number | undefined;
82
+ spatialRel?: "intersects" | "contains" | "within" | "crosses" | "touches" | "overlaps" | "bbox-intersects" | undefined;
24
83
  }>;
25
84
  export type Input = z.infer<typeof schema>;
85
+ interface CountOutcome {
86
+ count: number;
87
+ strategy: "queryExtent" | "totalCount";
88
+ degraded: ReturnType<typeof projectDegraded>;
89
+ }
90
+ /** Resolve a feature count through the canonical `Source` surface. */
91
+ export declare function countThroughSource(source: CapabilityAwareSource, query: Query, protocol: string, sourceId: string): Promise<CountOutcome>;
26
92
  export declare function execute(client: HonuaClient, input: Input): Promise<{
27
93
  content: Array<{
28
94
  type: "text";
29
95
  text: string;
30
96
  }>;
97
+ } | {
98
+ structuredContent: Record<string, unknown>;
99
+ isError: true;
100
+ content: Array<{
101
+ type: "text";
102
+ text: string;
103
+ }>;
31
104
  }>;
105
+ export {};
32
106
  //# sourceMappingURL=count-features.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"count-features.d.ts","sourceRoot":"","sources":["../../../src/tools/count-features.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;;;;;GAqB9D"}
1
+ {"version":3,"file":"count-features.d.ts","sourceRoot":"","sources":["../../../src/tools/count-features.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAEjD,OAAO,KAAK,EAAE,qBAAqB,EAAE,KAAK,EAAE,MAAM,wBAAwB,CAAC;AAC3E,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAGvD;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAGjB,CAAC;AAEH,MAAM,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,MAAM,CAAC,CAAC;AAE3C,UAAU,YAAY;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,aAAa,GAAG,YAAY,CAAC;IACvC,QAAQ,EAAE,UAAU,CAAC,OAAO,eAAe,CAAC,CAAC;CAC9C;AAED,sEAAsE;AACtE,wBAAsB,kBAAkB,CACtC,MAAM,EAAE,qBAAqB,EAC7B,KAAK,EAAE,KAAK,EACZ,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,YAAY,CAAC,CAevB;AAED,wBAAsB,OAAO,CAAC,MAAM,EAAE,WAAW,EAAE,KAAK,EAAE,KAAK;;;;;;;;;;;;GAmB9D"}
@@ -1,36 +1,54 @@
1
+ import { HonuaCapabilityNotSupportedError } from "@honua/sdk-js";
1
2
  import { z } from "zod";
2
- import { GEOMETRY_TYPES, coerceCount, jsonText, mapSpatialRel, resolveGeometryType } from "../helpers.js";
3
+ import { jsonText } from "../helpers.js";
4
+ import { withCapabilityHonesty } from "../neutral/errors.js";
5
+ import { queryFilterFields, toQuery } from "../neutral/query.js";
6
+ import { projectDegraded } from "../neutral/result.js";
7
+ import { resolveSource, sourceRefFields } from "../neutral/source-ref.js";
8
+ /**
9
+ * `honua_count_features` — protocol-neutral cardinality check (#1005).
10
+ *
11
+ * There is no single "count" verb across the protocols, so the count is taken
12
+ * from whichever canonical path the source's declared capabilities support:
13
+ *
14
+ * 1. `queryExtent` (GeoServices, WFS, gRPC) returns an exact server count
15
+ * alongside the envelope in one round trip.
16
+ * 2. otherwise a single-record `query` reports `totalCount`, which is where
17
+ * OGC API Features / STAC / OData carry `numberMatched`.
18
+ *
19
+ * When neither path yields a number the tool refuses with a structured
20
+ * capability error rather than reporting `0`.
21
+ */
3
22
  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, e.g. \"status = 'active'\""),
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)"),
23
+ ...sourceRefFields,
24
+ ...queryFilterFields,
16
25
  });
17
- 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
- outFields: "OBJECTID",
27
- extraParams: { returnCountOnly: true },
28
- }));
29
- // The default grpc-web transport returns very large counts as a string to
30
- // preserve precision beyond Number.MAX_SAFE_INTEGER; accept both forms.
31
- const count = coerceCount(response.count);
32
- if (count === undefined) {
33
- throw new Error("Count query did not return a numeric count.");
26
+ /** Resolve a feature count through the canonical `Source` surface. */
27
+ export async function countThroughSource(source, query, protocol, sourceId) {
28
+ if (source.capabilities.has("queryExtent")) {
29
+ const extent = await source.queryExtent(query);
30
+ if (typeof extent.count === "number" && Number.isFinite(extent.count)) {
31
+ return { count: extent.count, strategy: "queryExtent", degraded: undefined };
32
+ }
33
+ }
34
+ const probe = { ...query, returnGeometry: false, pagination: { ...query.pagination, limit: 1 } };
35
+ const result = await source.query(probe);
36
+ if (typeof result.totalCount === "number" && Number.isFinite(result.totalCount)) {
37
+ return { count: result.totalCount, strategy: "totalCount", degraded: projectDegraded(result.degraded) };
34
38
  }
35
- return jsonText({ count });
39
+ throw new HonuaCapabilityNotSupportedError("count", protocol, sourceId);
40
+ }
41
+ export async function execute(client, input) {
42
+ return withCapabilityHonesty(async () => {
43
+ const resolved = resolveSource(client, input);
44
+ const query = toQuery(input, { protocol: resolved.descriptor.protocol, paginate: false });
45
+ const outcome = await countThroughSource(resolved.source, query, resolved.descriptor.protocol, resolved.descriptor.id);
46
+ return jsonText({
47
+ source: resolved.ref.ref,
48
+ protocol: resolved.descriptor.protocol,
49
+ count: outcome.count,
50
+ countStrategy: outcome.strategy,
51
+ ...(outcome.degraded ? { degraded: outcome.degraded } : {}),
52
+ });
53
+ });
36
54
  }
@@ -1,14 +1,31 @@
1
1
  import type { HonuaClient } from "@honua/sdk-js";
2
2
  import { z } from "zod";
3
+ /**
4
+ * `honua_describe_layer` — protocol-neutral source description (#1005).
5
+ *
6
+ * The envelope is the same for every protocol: the neutral source reference,
7
+ * its protocol, the capabilities the runtime will actually honor, and a field
8
+ * schema. Where the schema comes from is protocol-specific (GeoServices layer
9
+ * metadata, an OGC collection + its queryables); the shape the agent reads is
10
+ * not. Protocols with no metadata document report `schemaAvailable: false`
11
+ * with a reason instead of an empty field list, which would read as "this
12
+ * source has no attributes".
13
+ */
3
14
  export declare const schema: z.ZodObject<{
4
- serviceId: z.ZodString;
5
- layerId: z.ZodNumber;
15
+ source: z.ZodOptional<z.ZodString>;
16
+ layout: z.ZodOptional<z.ZodEnum<["honua-facade", "ogc-api", "auto", "stac-api", "stac-static"]>>;
17
+ serviceId: z.ZodOptional<z.ZodString>;
18
+ layerId: z.ZodOptional<z.ZodNumber>;
6
19
  }, "strip", z.ZodTypeAny, {
7
- serviceId: string;
8
- layerId: number;
20
+ serviceId?: string | undefined;
21
+ layout?: "honua-facade" | "ogc-api" | "auto" | "stac-api" | "stac-static" | undefined;
22
+ layerId?: number | undefined;
23
+ source?: string | undefined;
9
24
  }, {
10
- serviceId: string;
11
- layerId: number;
25
+ serviceId?: string | undefined;
26
+ layout?: "honua-facade" | "ogc-api" | "auto" | "stac-api" | "stac-static" | undefined;
27
+ layerId?: number | undefined;
28
+ source?: string | undefined;
12
29
  }>;
13
30
  export type Input = z.infer<typeof schema>;
14
31
  export declare function execute(client: HonuaClient, input: Input): Promise<{
@@ -16,5 +33,12 @@ export declare function execute(client: HonuaClient, input: Input): Promise<{
16
33
  type: "text";
17
34
  text: string;
18
35
  }>;
36
+ } | {
37
+ structuredContent: Record<string, unknown>;
38
+ isError: true;
39
+ content: Array<{
40
+ type: "text";
41
+ text: string;
42
+ }>;
19
43
  }>;
20
44
  //# sourceMappingURL=describe-layer.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"describe-layer.d.ts","sourceRoot":"","sources":["../../../src/tools/describe-layer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AACjD,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,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;;;;;GAa9D"}
1
+ {"version":3,"file":"describe-layer.d.ts","sourceRoot":"","sources":["../../../src/tools/describe-layer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AACjD,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAKxB;;;;;;;;;;GAUG;AACH,eAAO,MAAM,MAAM;;;;;;;;;;;;;;;EAEjB,CAAC;AAEH,MAAM,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,MAAM,CAAC,CAAC;AA+F3C,wBAAsB,OAAO,CAAC,MAAM,EAAE,WAAW,EAAE,KAAK,EAAE,KAAK;;;;;;;;;;;;GAoB9D"}