@honua/mcp-server 0.0.6-alpha.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.
package/README.md ADDED
@@ -0,0 +1,42 @@
1
+ # `@honua/mcp-server`
2
+
3
+ Model Context Protocol (MCP) server for Honua geospatial feature services.
4
+
5
+ This package exposes a focused MCP surface for discovery and query workflows on top of Honua's FeatureServer APIs.
6
+
7
+ ## Requirements
8
+
9
+ - Node.js `>=20`
10
+ - A reachable Honua server URL
11
+
12
+ ## Environment Variables
13
+
14
+ - `HONUA_BASE_URL` (required): absolute Honua base URL, for example `https://honua.example.com`
15
+ - `HONUA_TRANSPORT` (optional): `grpc-web` (default) or `rest`
16
+ - `HONUA_API_KEY` (optional): admin/API key when your deployment requires it
17
+ - `HONUA_TIMEOUT_MS` (optional): request timeout in milliseconds (default `30000`)
18
+ - `HONUA_RETRY_MAX_RETRIES` (optional): retry attempts for transient failures (default `2`)
19
+
20
+ When `HONUA_API_KEY` is configured, use `https://` for non-localhost servers.
21
+
22
+ ## Run Locally
23
+
24
+ ```bash
25
+ npm install
26
+ npm run build
27
+ HONUA_BASE_URL="https://honua.example.com" HONUA_TRANSPORT="grpc-web" node dist/src/index.js
28
+ ```
29
+
30
+ ## MCP Tools
31
+
32
+ - `honua_list_services`
33
+ - `honua_describe_layer`
34
+ - `honua_query_features`
35
+ - `honua_count_features`
36
+ - `honua_get_extent`
37
+ - `honua_statistics`
38
+
39
+ ## MCP Resources
40
+
41
+ - `honua://services`
42
+ - `honua://services/{encodedServiceId}/layers/{layerId}`
@@ -0,0 +1,17 @@
1
+ import type { EsriGeometryType, EsriSpatialRel } from "@honua/sdk-js";
2
+ export declare const GEOMETRY_TYPES: readonly ["esriGeometryPoint", "esriGeometryPolyline", "esriGeometryPolygon", "esriGeometryEnvelope", "esriGeometryMultipoint"];
3
+ export declare function mapSpatialRel(rel: string | undefined): EsriSpatialRel | undefined;
4
+ export declare function clampLimit(limit: number | undefined): number;
5
+ export declare function resolveGeometryType(geometry: Record<string, unknown> | undefined, geometryType: EsriGeometryType | undefined): EsriGeometryType | undefined;
6
+ export declare function jsonText(result: unknown): {
7
+ content: Array<{
8
+ type: "text";
9
+ text: string;
10
+ }>;
11
+ };
12
+ export declare function metadataErrorText(): string;
13
+ export declare function encodeServiceId(serviceId: string): string;
14
+ export declare function decodeServiceId(encoded: string): string;
15
+ export declare function parseLayerId(value: string): number;
16
+ export declare function mapWithConcurrency<T, R>(items: readonly T[], concurrency: number, fn: (item: T, index: number) => Promise<R>): Promise<R[]>;
17
+ //# sourceMappingURL=helpers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../../src/helpers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAMtE,eAAO,MAAM,cAAc,iIAMjB,CAAC;AAQX,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,cAAc,GAAG,SAAS,CAKjF;AAED,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAG5D;AAED,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,EAC7C,YAAY,EAAE,gBAAgB,GAAG,SAAS,GACzC,gBAAgB,GAAG,SAAS,CA8B9B;AAED,wBAAgB,QAAQ,CAAC,MAAM,EAAE,OAAO,GAAG;IAAE,OAAO,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CAAE,CAE5F;AAED,wBAAgB,iBAAiB,IAAI,MAAM,CAE1C;AAED,wBAAgB,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAEzD;AAED,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAMvD;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAKlD;AAED,wBAAsB,kBAAkB,CAAC,CAAC,EAAE,CAAC,EAC3C,KAAK,EAAE,SAAS,CAAC,EAAE,EACnB,WAAW,EAAE,MAAM,EACnB,EAAE,EAAE,CAAC,IAAI,EAAE,CAAC,EAAE,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,GACzC,OAAO,CAAC,CAAC,EAAE,CAAC,CAqBd"}
@@ -0,0 +1,93 @@
1
+ const MAX_LIMIT = 2000;
2
+ const DEFAULT_LIMIT = 100;
3
+ const METADATA_ERROR_MESSAGE = "Failed to fetch service metadata.";
4
+ export const GEOMETRY_TYPES = [
5
+ "esriGeometryPoint",
6
+ "esriGeometryPolyline",
7
+ "esriGeometryPolygon",
8
+ "esriGeometryEnvelope",
9
+ "esriGeometryMultipoint",
10
+ ];
11
+ const SPATIAL_REL_MAP = {
12
+ intersects: "esriSpatialRelIntersects",
13
+ contains: "esriSpatialRelContains",
14
+ within: "esriSpatialRelWithin",
15
+ };
16
+ export function mapSpatialRel(rel) {
17
+ if (!rel)
18
+ return undefined;
19
+ const mapped = SPATIAL_REL_MAP[rel];
20
+ if (!mapped)
21
+ throw new Error(`Unknown spatialRel "${rel}". Expected: intersects, contains, within`);
22
+ return mapped;
23
+ }
24
+ export function clampLimit(limit) {
25
+ const n = limit ?? DEFAULT_LIMIT;
26
+ return Math.min(Math.max(1, n), MAX_LIMIT);
27
+ }
28
+ export function resolveGeometryType(geometry, geometryType) {
29
+ if (geometryType) {
30
+ return geometryType;
31
+ }
32
+ if (!geometry) {
33
+ return undefined;
34
+ }
35
+ if ("xmin" in geometry && "xmax" in geometry && "ymin" in geometry && "ymax" in geometry) {
36
+ return "esriGeometryEnvelope";
37
+ }
38
+ if ("x" in geometry && "y" in geometry) {
39
+ return "esriGeometryPoint";
40
+ }
41
+ if (Array.isArray(geometry.rings)) {
42
+ return "esriGeometryPolygon";
43
+ }
44
+ if (Array.isArray(geometry.paths)) {
45
+ return "esriGeometryPolyline";
46
+ }
47
+ if (Array.isArray(geometry.points)) {
48
+ return "esriGeometryMultipoint";
49
+ }
50
+ return undefined;
51
+ }
52
+ export function jsonText(result) {
53
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
54
+ }
55
+ export function metadataErrorText() {
56
+ return METADATA_ERROR_MESSAGE;
57
+ }
58
+ export function encodeServiceId(serviceId) {
59
+ return encodeURIComponent(serviceId);
60
+ }
61
+ export function decodeServiceId(encoded) {
62
+ try {
63
+ return decodeURIComponent(encoded);
64
+ }
65
+ catch {
66
+ throw new Error(`Invalid encoded serviceId: "${encoded}"`);
67
+ }
68
+ }
69
+ export function parseLayerId(value) {
70
+ if (!/^\d+$/.test(value)) {
71
+ throw new Error(`Invalid layerId: "${value}"`);
72
+ }
73
+ return Number.parseInt(value, 10);
74
+ }
75
+ export async function mapWithConcurrency(items, concurrency, fn) {
76
+ if (concurrency < 1 || !Number.isFinite(concurrency)) {
77
+ throw new Error(`Invalid concurrency value: ${concurrency}`);
78
+ }
79
+ const results = new Array(items.length);
80
+ let cursor = 0;
81
+ const workers = Array.from({ length: Math.min(concurrency, items.length || 1) }, async () => {
82
+ while (true) {
83
+ const index = cursor;
84
+ cursor += 1;
85
+ if (index >= items.length) {
86
+ return;
87
+ }
88
+ results[index] = await fn(items[index], index);
89
+ }
90
+ });
91
+ await Promise.all(workers);
92
+ return results;
93
+ }
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env node
2
+ import { HonuaClient } from "@honua/sdk-js";
3
+ import type { HonuaTransport } from "@honua/sdk-js";
4
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
+ export interface RuntimeOptions {
6
+ baseUrl: string;
7
+ apiKey: string | undefined;
8
+ transport: HonuaTransport;
9
+ timeoutMs: number;
10
+ retryMaxRetries: number;
11
+ }
12
+ export declare const SERVER_VERSION: string;
13
+ export declare function resolveRuntimeOptions(env: NodeJS.ProcessEnv): RuntimeOptions;
14
+ export declare function createClientFromEnv(env?: NodeJS.ProcessEnv): HonuaClient;
15
+ export declare function createServer(client: HonuaClient): McpServer;
16
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":";AAGA,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AACpD,OAAO,EAAE,SAAS,EAAoB,MAAM,yCAAyC,CAAC;AAYtF,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,SAAS,EAAE,cAAc,CAAC;IAC1B,SAAS,EAAE,MAAM,CAAC;IAClB,eAAe,EAAE,MAAM,CAAC;CACzB;AAMD,eAAO,MAAM,cAAc,QAAyB,CAAC;AA2DrD,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,MAAM,CAAC,UAAU,GAAG,cAAc,CAgD5E;AAED,wBAAgB,mBAAmB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,WAAW,CASrF;AAED,wBAAgB,YAAY,CAAC,MAAM,EAAE,WAAW,aA8D/C"}
@@ -0,0 +1,143 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync } from "node:fs";
3
+ import { fileURLToPath } from "node:url";
4
+ import { HonuaClient } from "@honua/sdk-js";
5
+ import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
6
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
7
+ import * as layerSchemaResource from "./resources/layer-schema.js";
8
+ import * as servicesResource from "./resources/services.js";
9
+ import * as countFeatures from "./tools/count-features.js";
10
+ import * as describeLayer from "./tools/describe-layer.js";
11
+ import * as getExtent from "./tools/get-extent.js";
12
+ import * as listServices from "./tools/list-services.js";
13
+ import * as queryFeatures from "./tools/query-features.js";
14
+ import * as statistics from "./tools/statistics.js";
15
+ const DEFAULT_TIMEOUT_MS = 30_000;
16
+ const DEFAULT_MAX_RETRIES = 2;
17
+ const DEFAULT_SERVER_VERSION = "0.0.3-alpha.0";
18
+ export const SERVER_VERSION = resolveServerVersion();
19
+ function resolveServerVersion() {
20
+ try {
21
+ const packageJsonPath = new URL("../../package.json", import.meta.url);
22
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
23
+ if (typeof packageJson.version === "string" && packageJson.version.length > 0) {
24
+ return packageJson.version;
25
+ }
26
+ }
27
+ catch {
28
+ // Fall back to a static version if package metadata is unavailable.
29
+ }
30
+ return DEFAULT_SERVER_VERSION;
31
+ }
32
+ function parsePositiveInteger(env, name, defaultValue) {
33
+ const raw = env[name];
34
+ if (raw === undefined) {
35
+ return defaultValue;
36
+ }
37
+ const normalized = raw.trim();
38
+ if (!/^\d+$/.test(normalized)) {
39
+ throw new Error(`${name} must be a positive integer, received "${raw}"`);
40
+ }
41
+ const parsed = Number.parseInt(normalized, 10);
42
+ if (!Number.isFinite(parsed) || parsed < 1) {
43
+ throw new Error(`${name} must be a positive integer, received "${raw}"`);
44
+ }
45
+ return parsed;
46
+ }
47
+ function parseNonNegativeInteger(env, name, defaultValue) {
48
+ const raw = env[name];
49
+ if (raw === undefined) {
50
+ return defaultValue;
51
+ }
52
+ const normalized = raw.trim();
53
+ if (!/^\d+$/.test(normalized)) {
54
+ throw new Error(`${name} must be a non-negative integer, received "${raw}"`);
55
+ }
56
+ const parsed = Number.parseInt(normalized, 10);
57
+ if (!Number.isFinite(parsed) || parsed < 0) {
58
+ throw new Error(`${name} must be a non-negative integer, received "${raw}"`);
59
+ }
60
+ return parsed;
61
+ }
62
+ function isLoopbackHost(hostname) {
63
+ const normalized = hostname.toLowerCase();
64
+ return normalized === "localhost" || normalized === "127.0.0.1" || normalized === "::1" || normalized === "[::1]";
65
+ }
66
+ export function resolveRuntimeOptions(env) {
67
+ const baseUrl = env.HONUA_BASE_URL;
68
+ if (!baseUrl) {
69
+ throw new Error("HONUA_BASE_URL environment variable is required.");
70
+ }
71
+ let parsedUrl;
72
+ try {
73
+ parsedUrl = new URL(baseUrl);
74
+ }
75
+ catch {
76
+ throw new Error(`HONUA_BASE_URL must be a valid absolute URL: ${baseUrl}`);
77
+ }
78
+ if (parsedUrl.protocol !== "http:" && parsedUrl.protocol !== "https:") {
79
+ throw new Error(`HONUA_BASE_URL must use http or https: ${baseUrl}`);
80
+ }
81
+ const apiKey = env.HONUA_API_KEY;
82
+ if (apiKey && parsedUrl.protocol === "http:" && !isLoopbackHost(parsedUrl.hostname)) {
83
+ throw new Error("HONUA_API_KEY over non-local HTTP is not allowed. Use HTTPS, or use localhost for local development.");
84
+ }
85
+ const rawTransportInput = env.HONUA_TRANSPORT ?? "grpc-web";
86
+ const normalizedTransport = rawTransportInput.trim().toLowerCase();
87
+ const transport = normalizedTransport === "grpc-web" || normalizedTransport === "grpc" || normalizedTransport === "grcp"
88
+ ? "grpc-web"
89
+ : normalizedTransport === "rest"
90
+ ? "rest"
91
+ : undefined;
92
+ if (!transport) {
93
+ throw new Error(`HONUA_TRANSPORT must be "grpc-web" (aliases: "grpc", "grcp") or "rest", received "${rawTransportInput}"`);
94
+ }
95
+ const timeoutMs = parsePositiveInteger(env, "HONUA_TIMEOUT_MS", DEFAULT_TIMEOUT_MS);
96
+ const retryMaxRetries = parseNonNegativeInteger(env, "HONUA_RETRY_MAX_RETRIES", DEFAULT_MAX_RETRIES);
97
+ return {
98
+ baseUrl: parsedUrl.toString(),
99
+ apiKey,
100
+ transport,
101
+ timeoutMs,
102
+ retryMaxRetries,
103
+ };
104
+ }
105
+ export function createClientFromEnv(env = process.env) {
106
+ const options = resolveRuntimeOptions(env);
107
+ return new HonuaClient({
108
+ baseUrl: options.baseUrl,
109
+ apiKey: options.apiKey,
110
+ transport: options.transport,
111
+ timeoutMs: options.timeoutMs,
112
+ retry: options.retryMaxRetries > 0 ? { maxRetries: options.retryMaxRetries } : undefined,
113
+ });
114
+ }
115
+ export function createServer(client) {
116
+ const server = new McpServer({
117
+ name: "honua",
118
+ version: SERVER_VERSION,
119
+ });
120
+ // ── Tools ──────────────────────────────────────────────────────
121
+ server.tool("honua_list_services", "Discover all available feature services. Set includeDetails=true for descriptions, layer counts, and spatial references.", listServices.schema.shape, async (args) => listServices.execute(client, listServices.schema.parse(args)));
122
+ server.tool("honua_describe_layer", "Get full schema for a layer — fields, geometry type, extent, relationships.", describeLayer.schema.shape, async (args) => describeLayer.execute(client, describeLayer.schema.parse(args)));
123
+ server.tool("honua_query_features", "Query features with attribute filters, spatial filters, field selection, and pagination. returnGeometry defaults to false to save tokens.", queryFeatures.schema.shape, async (args) => queryFeatures.execute(client, queryFeatures.schema.parse(args)));
124
+ server.tool("honua_count_features", "Count features matching a filter without returning data. Use before querying to check cardinality.", countFeatures.schema.shape, async (args) => countFeatures.execute(client, countFeatures.schema.parse(args)));
125
+ server.tool("honua_get_extent", "Get the spatial bounding box of features matching a filter.", getExtent.schema.shape, async (args) => getExtent.execute(client, getExtent.schema.parse(args)));
126
+ server.tool("honua_statistics", "Compute aggregate statistics (count, sum, avg, min, max, stddev) on a field, optionally grouped.", statistics.schema.shape, async (args) => statistics.execute(client, statistics.schema.parse(args)));
127
+ // ── Resources ──────────────────────────────────────────────────
128
+ server.resource("services-catalog", servicesResource.uri, async (uri) => servicesResource.read(client));
129
+ server.resource("layer-schema", new ResourceTemplate(layerSchemaResource.uriTemplate, { list: undefined }), async (uri, params) => layerSchemaResource.read(client, params.encodedServiceId, params.layerId));
130
+ return server;
131
+ }
132
+ async function main() {
133
+ const client = createClientFromEnv();
134
+ const server = createServer(client);
135
+ const transport = new StdioServerTransport();
136
+ await server.connect(transport);
137
+ }
138
+ if (process.argv[1] === fileURLToPath(import.meta.url)) {
139
+ main().catch((err) => {
140
+ process.stderr.write(`Fatal: ${err instanceof Error ? err.message : String(err)}\n`);
141
+ process.exit(1);
142
+ });
143
+ }
@@ -0,0 +1,10 @@
1
+ import type { HonuaClient } from "@honua/sdk-js";
2
+ export declare const uriTemplate = "honua://services/{encodedServiceId}/layers/{layerId}";
3
+ export declare function read(client: HonuaClient, encodedServiceId: string, layerId: string): Promise<{
4
+ contents: {
5
+ uri: string;
6
+ mimeType: "application/json";
7
+ text: string;
8
+ }[];
9
+ }>;
10
+ //# sourceMappingURL=layer-schema.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"layer-schema.d.ts","sourceRoot":"","sources":["../../../src/resources/layer-schema.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAGjD,eAAO,MAAM,WAAW,yDAAyD,CAAC;AAElF,wBAAsB,IAAI,CAAC,MAAM,EAAE,WAAW,EAAE,gBAAgB,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM;;;;;;GA4BxF"}
@@ -0,0 +1,27 @@
1
+ import { decodeServiceId, parseLayerId } from "../helpers.js";
2
+ export const uriTemplate = "honua://services/{encodedServiceId}/layers/{layerId}";
3
+ export async function read(client, encodedServiceId, layerId) {
4
+ const serviceId = decodeServiceId(encodedServiceId);
5
+ const layerIdNum = parseLayerId(layerId);
6
+ const meta = await client.getLayerMetadata(serviceId, layerIdNum);
7
+ const schema = {
8
+ id: meta.id,
9
+ name: meta.name,
10
+ description: meta.description ?? null,
11
+ geometryType: meta.geometryType ?? null,
12
+ fields: (meta.fields ?? []).map((f) => ({ name: f.name, type: f.type, alias: f.alias ?? f.name })),
13
+ extent: meta.extent ?? null,
14
+ spatialReference: meta.spatialReference ?? null,
15
+ relationships: meta.relationships ?? [],
16
+ };
17
+ const uri = `honua://services/${encodedServiceId}/layers/${layerId}`;
18
+ return {
19
+ contents: [
20
+ {
21
+ uri,
22
+ mimeType: "application/json",
23
+ text: JSON.stringify(schema, null, 2),
24
+ },
25
+ ],
26
+ };
27
+ }
@@ -0,0 +1,10 @@
1
+ import type { HonuaClient } from "@honua/sdk-js";
2
+ export declare const uri = "honua://services";
3
+ export declare function read(client: HonuaClient): Promise<{
4
+ contents: {
5
+ uri: string;
6
+ mimeType: "application/json";
7
+ text: string;
8
+ }[];
9
+ }>;
10
+ //# sourceMappingURL=services.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"services.d.ts","sourceRoot":"","sources":["../../../src/resources/services.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAGjD,eAAO,MAAM,GAAG,qBAAqB,CAAC;AAItC,wBAAsB,IAAI,CAAC,MAAM,EAAE,WAAW;;;;;;GAoC7C"}
@@ -0,0 +1,39 @@
1
+ import { encodeServiceId, mapWithConcurrency, metadataErrorText } from "../helpers.js";
2
+ export const uri = "honua://services";
3
+ const METADATA_CONCURRENCY = 8;
4
+ export async function read(client) {
5
+ const response = await client.listServices();
6
+ const featureServices = (response.services ?? []).filter((s) => s.type === "FeatureServer");
7
+ const catalog = await mapWithConcurrency(featureServices, METADATA_CONCURRENCY, async (s) => {
8
+ try {
9
+ const meta = await client.getFeatureServiceMetadata(s.name);
10
+ return {
11
+ serviceId: s.name,
12
+ encodedServiceId: encodeServiceId(s.name),
13
+ type: s.type,
14
+ layers: meta.layers ?? [],
15
+ tables: meta.tables ?? [],
16
+ metadataError: null,
17
+ };
18
+ }
19
+ catch {
20
+ return {
21
+ serviceId: s.name,
22
+ encodedServiceId: encodeServiceId(s.name),
23
+ type: s.type,
24
+ layers: [],
25
+ tables: [],
26
+ metadataError: metadataErrorText(),
27
+ };
28
+ }
29
+ });
30
+ return {
31
+ contents: [
32
+ {
33
+ uri,
34
+ mimeType: "application/json",
35
+ text: JSON.stringify(catalog, null, 2),
36
+ },
37
+ ],
38
+ };
39
+ }
@@ -0,0 +1,32 @@
1
+ import type { HonuaClient } from "@honua/sdk-js";
2
+ import { z } from "zod";
3
+ export declare const schema: z.ZodObject<{
4
+ serviceId: z.ZodString;
5
+ layerId: z.ZodNumber;
6
+ where: z.ZodOptional<z.ZodString>;
7
+ geometry: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
8
+ geometryType: z.ZodOptional<z.ZodEnum<["esriGeometryPoint", "esriGeometryPolyline", "esriGeometryPolygon", "esriGeometryEnvelope", "esriGeometryMultipoint"]>>;
9
+ spatialRel: z.ZodOptional<z.ZodEnum<["intersects", "contains", "within"]>>;
10
+ }, "strip", z.ZodTypeAny, {
11
+ serviceId: string;
12
+ layerId: number;
13
+ where?: string | undefined;
14
+ geometry?: Record<string, unknown> | undefined;
15
+ geometryType?: "esriGeometryPoint" | "esriGeometryPolyline" | "esriGeometryPolygon" | "esriGeometryEnvelope" | "esriGeometryMultipoint" | undefined;
16
+ spatialRel?: "intersects" | "contains" | "within" | undefined;
17
+ }, {
18
+ serviceId: string;
19
+ layerId: number;
20
+ where?: string | undefined;
21
+ geometry?: Record<string, unknown> | undefined;
22
+ geometryType?: "esriGeometryPoint" | "esriGeometryPolyline" | "esriGeometryPolygon" | "esriGeometryEnvelope" | "esriGeometryMultipoint" | undefined;
23
+ spatialRel?: "intersects" | "contains" | "within" | undefined;
24
+ }>;
25
+ export type Input = z.infer<typeof schema>;
26
+ export declare function execute(client: HonuaClient, input: Input): Promise<{
27
+ content: Array<{
28
+ type: "text";
29
+ text: string;
30
+ }>;
31
+ }>;
32
+ //# sourceMappingURL=count-features.d.ts.map
@@ -0,0 +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;;;;;GAoB9D"}
@@ -0,0 +1,35 @@
1
+ import { z } from "zod";
2
+ import { GEOMETRY_TYPES, jsonText, mapSpatialRel, resolveGeometryType } from "../helpers.js";
3
+ 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)"),
16
+ });
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
+ const hasCount = Object.prototype.hasOwnProperty.call(response, "count");
30
+ const count = response.count;
31
+ if (!hasCount || typeof count !== "number" || !Number.isFinite(count)) {
32
+ throw new Error("Count query did not return a numeric count.");
33
+ }
34
+ return jsonText({ count });
35
+ }
@@ -0,0 +1,20 @@
1
+ import type { HonuaClient } from "@honua/sdk-js";
2
+ import { z } from "zod";
3
+ export declare const schema: z.ZodObject<{
4
+ serviceId: z.ZodString;
5
+ layerId: z.ZodNumber;
6
+ }, "strip", z.ZodTypeAny, {
7
+ serviceId: string;
8
+ layerId: number;
9
+ }, {
10
+ serviceId: string;
11
+ layerId: number;
12
+ }>;
13
+ export type Input = z.infer<typeof schema>;
14
+ export declare function execute(client: HonuaClient, input: Input): Promise<{
15
+ content: Array<{
16
+ type: "text";
17
+ text: string;
18
+ }>;
19
+ }>;
20
+ //# sourceMappingURL=describe-layer.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,19 @@
1
+ import { z } from "zod";
2
+ import { jsonText } from "../helpers.js";
3
+ 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
+ });
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,
12
+ description: meta.description ?? null,
13
+ geometryType: meta.geometryType ?? null,
14
+ fields: (meta.fields ?? []).map((f) => ({ name: f.name, type: f.type, alias: f.alias ?? f.name })),
15
+ extent: meta.extent ?? null,
16
+ spatialReference: meta.spatialReference ?? null,
17
+ relationships: meta.relationships ?? [],
18
+ });
19
+ }
@@ -0,0 +1,32 @@
1
+ import type { HonuaClient } from "@honua/sdk-js";
2
+ import { z } from "zod";
3
+ export declare const schema: z.ZodObject<{
4
+ serviceId: z.ZodString;
5
+ layerId: z.ZodNumber;
6
+ where: z.ZodOptional<z.ZodString>;
7
+ geometry: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
8
+ geometryType: z.ZodOptional<z.ZodEnum<["esriGeometryPoint", "esriGeometryPolyline", "esriGeometryPolygon", "esriGeometryEnvelope", "esriGeometryMultipoint"]>>;
9
+ spatialRel: z.ZodOptional<z.ZodEnum<["intersects", "contains", "within"]>>;
10
+ }, "strip", z.ZodTypeAny, {
11
+ serviceId: string;
12
+ layerId: number;
13
+ where?: string | undefined;
14
+ geometry?: Record<string, unknown> | undefined;
15
+ geometryType?: "esriGeometryPoint" | "esriGeometryPolyline" | "esriGeometryPolygon" | "esriGeometryEnvelope" | "esriGeometryMultipoint" | undefined;
16
+ spatialRel?: "intersects" | "contains" | "within" | undefined;
17
+ }, {
18
+ serviceId: string;
19
+ layerId: number;
20
+ where?: string | undefined;
21
+ geometry?: Record<string, unknown> | undefined;
22
+ geometryType?: "esriGeometryPoint" | "esriGeometryPolyline" | "esriGeometryPolygon" | "esriGeometryEnvelope" | "esriGeometryMultipoint" | undefined;
23
+ spatialRel?: "intersects" | "contains" | "within" | undefined;
24
+ }>;
25
+ export type Input = z.infer<typeof schema>;
26
+ export declare function execute(client: HonuaClient, input: Input): Promise<{
27
+ content: Array<{
28
+ type: "text";
29
+ text: string;
30
+ }>;
31
+ }>;
32
+ //# sourceMappingURL=get-extent.d.ts.map
@@ -0,0 +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;;;;;GAmC9D"}
@@ -0,0 +1,49 @@
1
+ import { z } from "zod";
2
+ import { GEOMETRY_TYPES, jsonText, mapSpatialRel, resolveGeometryType } from "../helpers.js";
3
+ 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)"),
16
+ });
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
+ extraParams: { returnExtentOnly: true },
27
+ }));
28
+ const hasCount = Object.prototype.hasOwnProperty.call(response, "count");
29
+ const count = hasCount && typeof response.count === "number" && Number.isFinite(response.count) ? response.count : undefined;
30
+ const hasExtent = Object.prototype.hasOwnProperty.call(response, "extent");
31
+ if (!hasExtent) {
32
+ // Some backends can return count-only payloads for empty extent queries.
33
+ if (count !== undefined) {
34
+ return jsonText({ extent: null, count });
35
+ }
36
+ throw new Error("Extent query did not return an extent payload.");
37
+ }
38
+ let extent;
39
+ if (response.extent === null) {
40
+ extent = null;
41
+ }
42
+ else if (typeof response.extent === "object") {
43
+ extent = response.extent;
44
+ }
45
+ else {
46
+ throw new Error("Extent query returned an invalid extent payload.");
47
+ }
48
+ return jsonText({ extent, count });
49
+ }
@@ -0,0 +1,17 @@
1
+ import type { HonuaClient } from "@honua/sdk-js";
2
+ import { z } from "zod";
3
+ export declare const schema: z.ZodObject<{
4
+ includeDetails: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
5
+ }, "strip", z.ZodTypeAny, {
6
+ includeDetails: boolean;
7
+ }, {
8
+ includeDetails?: boolean | undefined;
9
+ }>;
10
+ export type Input = z.infer<typeof schema>;
11
+ export declare function execute(client: HonuaClient, input: Input): Promise<{
12
+ content: Array<{
13
+ type: "text";
14
+ text: string;
15
+ }>;
16
+ }>;
17
+ //# sourceMappingURL=list-services.d.ts.map
@@ -0,0 +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;;;;;GAgC9D"}
@@ -0,0 +1,41 @@
1
+ import { z } from "zod";
2
+ import { jsonText, mapWithConcurrency, metadataErrorText } from "../helpers.js";
3
+ export const schema = z.object({
4
+ includeDetails: z
5
+ .boolean()
6
+ .optional()
7
+ .default(false)
8
+ .describe("Fetch service metadata (description, layer count, spatial reference). Slower due to per-service requests."),
9
+ });
10
+ const METADATA_CONCURRENCY = 8;
11
+ export async function execute(client, input) {
12
+ const response = await client.listServices();
13
+ const services = (response.services ?? []).filter((s) => s.type === "FeatureServer");
14
+ if (!input.includeDetails) {
15
+ return jsonText(services.map((s) => ({ serviceId: s.name, type: s.type })));
16
+ }
17
+ const detailed = await mapWithConcurrency(services, METADATA_CONCURRENCY, async (s) => {
18
+ try {
19
+ const meta = await client.getFeatureServiceMetadata(s.name);
20
+ return {
21
+ serviceId: s.name,
22
+ type: s.type,
23
+ description: meta.serviceDescription ?? null,
24
+ layerCount: (meta.layers?.length ?? 0) + (meta.tables?.length ?? 0),
25
+ spatialReference: meta.spatialReference ?? null,
26
+ metadataError: null,
27
+ };
28
+ }
29
+ catch {
30
+ return {
31
+ serviceId: s.name,
32
+ type: s.type,
33
+ description: null,
34
+ layerCount: null,
35
+ spatialReference: null,
36
+ metadataError: metadataErrorText(),
37
+ };
38
+ }
39
+ });
40
+ return jsonText(detailed);
41
+ }
@@ -0,0 +1,47 @@
1
+ import type { HonuaClient } from "@honua/sdk-js";
2
+ import { z } from "zod";
3
+ export declare const schema: z.ZodObject<{
4
+ serviceId: z.ZodString;
5
+ layerId: z.ZodNumber;
6
+ where: z.ZodOptional<z.ZodString>;
7
+ outFields: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
8
+ geometry: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
9
+ geometryType: z.ZodOptional<z.ZodEnum<["esriGeometryPoint", "esriGeometryPolyline", "esriGeometryPolygon", "esriGeometryEnvelope", "esriGeometryMultipoint"]>>;
10
+ spatialRel: z.ZodOptional<z.ZodEnum<["intersects", "contains", "within"]>>;
11
+ orderBy: z.ZodOptional<z.ZodString>;
12
+ limit: z.ZodOptional<z.ZodNumber>;
13
+ offset: z.ZodOptional<z.ZodNumber>;
14
+ returnGeometry: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
15
+ }, "strip", z.ZodTypeAny, {
16
+ serviceId: string;
17
+ layerId: number;
18
+ returnGeometry: boolean;
19
+ where?: string | undefined;
20
+ geometry?: Record<string, unknown> | undefined;
21
+ geometryType?: "esriGeometryPoint" | "esriGeometryPolyline" | "esriGeometryPolygon" | "esriGeometryEnvelope" | "esriGeometryMultipoint" | undefined;
22
+ spatialRel?: "intersects" | "contains" | "within" | undefined;
23
+ outFields?: string[] | undefined;
24
+ orderBy?: string | undefined;
25
+ limit?: number | undefined;
26
+ offset?: number | undefined;
27
+ }, {
28
+ serviceId: string;
29
+ layerId: number;
30
+ where?: string | undefined;
31
+ geometry?: Record<string, unknown> | undefined;
32
+ geometryType?: "esriGeometryPoint" | "esriGeometryPolyline" | "esriGeometryPolygon" | "esriGeometryEnvelope" | "esriGeometryMultipoint" | undefined;
33
+ spatialRel?: "intersects" | "contains" | "within" | undefined;
34
+ outFields?: string[] | undefined;
35
+ orderBy?: string | undefined;
36
+ limit?: number | undefined;
37
+ offset?: number | undefined;
38
+ returnGeometry?: boolean | undefined;
39
+ }>;
40
+ export type Input = z.infer<typeof schema>;
41
+ export declare function execute(client: HonuaClient, input: Input): Promise<{
42
+ content: Array<{
43
+ type: "text";
44
+ text: string;
45
+ }>;
46
+ }>;
47
+ //# sourceMappingURL=query-features.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"query-features.d.ts","sourceRoot":"","sources":["../../../src/tools/query-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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAkBjB,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;;;;;GA4B9D"}
@@ -0,0 +1,48 @@
1
+ import { z } from "zod";
2
+ import { GEOMETRY_TYPES, clampLimit, jsonText, mapSpatialRel, resolveGeometryType } from "../helpers.js";
3
+ 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. "population > 50000"'),
7
+ outFields: z.array(z.string()).optional().describe("Fields to return; defaults to all"),
8
+ geometry: z.record(z.unknown()).optional().describe("Esri JSON geometry for spatial filter"),
9
+ geometryType: z
10
+ .enum(GEOMETRY_TYPES)
11
+ .optional()
12
+ .describe("Esri geometry type. If omitted, inferred from geometry when possible."),
13
+ spatialRel: z
14
+ .enum(["intersects", "contains", "within"])
15
+ .optional()
16
+ .describe("Spatial relationship (default: intersects)"),
17
+ orderBy: z.string().optional().describe('Sort order, e.g. "name ASC"'),
18
+ limit: z.number().int().positive().optional().describe("Max features to return (default 100, max 2000)"),
19
+ offset: z.number().int().nonnegative().optional().describe("Number of features to skip (for pagination)"),
20
+ returnGeometry: z.boolean().optional().default(false).describe("Include geometry in results (default: false)"),
21
+ });
22
+ export async function execute(client, input) {
23
+ const response = await client.queryFeatures({
24
+ serviceId: input.serviceId,
25
+ layerId: input.layerId,
26
+ where: input.where,
27
+ outFields: input.outFields ?? "*",
28
+ geometry: input.geometry,
29
+ geometryType: resolveGeometryType(input.geometry, input.geometryType),
30
+ spatialRel: mapSpatialRel(input.spatialRel),
31
+ orderByFields: input.orderBy,
32
+ resultRecordCount: clampLimit(input.limit),
33
+ resultOffset: input.offset,
34
+ returnGeometry: input.returnGeometry,
35
+ });
36
+ const features = response.features ?? [];
37
+ return jsonText({
38
+ returnedCount: features.length,
39
+ features: features.map((f) => ({
40
+ attributes: f.attributes,
41
+ ...(input.returnGeometry && f.geometry ? { geometry: f.geometry } : {}),
42
+ })),
43
+ exceededTransferLimit: response.exceededTransferLimit ?? false,
44
+ objectIdFieldName: response.objectIdFieldName ?? null,
45
+ geometryType: response.geometryType ?? null,
46
+ spatialReference: response.spatialReference ?? null,
47
+ });
48
+ }
@@ -0,0 +1,32 @@
1
+ import type { HonuaClient } from "@honua/sdk-js";
2
+ import { z } from "zod";
3
+ export declare const schema: z.ZodObject<{
4
+ serviceId: z.ZodString;
5
+ layerId: z.ZodNumber;
6
+ statisticType: z.ZodEnum<["count", "sum", "avg", "min", "max", "stddev"]>;
7
+ onField: z.ZodString;
8
+ groupBy: z.ZodOptional<z.ZodString>;
9
+ where: z.ZodOptional<z.ZodString>;
10
+ }, "strip", z.ZodTypeAny, {
11
+ serviceId: string;
12
+ layerId: number;
13
+ statisticType: "count" | "sum" | "avg" | "min" | "max" | "stddev";
14
+ onField: string;
15
+ where?: string | undefined;
16
+ groupBy?: string | undefined;
17
+ }, {
18
+ serviceId: string;
19
+ layerId: number;
20
+ statisticType: "count" | "sum" | "avg" | "min" | "max" | "stddev";
21
+ onField: string;
22
+ where?: string | undefined;
23
+ groupBy?: string | undefined;
24
+ }>;
25
+ export type Input = z.infer<typeof schema>;
26
+ export declare function execute(client: HonuaClient, input: Input): Promise<{
27
+ content: Array<{
28
+ type: "text";
29
+ text: string;
30
+ }>;
31
+ }>;
32
+ //# sourceMappingURL=statistics.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"statistics.d.ts","sourceRoot":"","sources":["../../../src/tools/statistics.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AACjD,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,eAAO,MAAM,MAAM;;;;;;;;;;;;;;;;;;;;;EAOjB,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;;;;;GAuB9D"}
@@ -0,0 +1,31 @@
1
+ import { z } from "zod";
2
+ import { jsonText } from "../helpers.js";
3
+ 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
+ statisticType: z.enum(["count", "sum", "avg", "min", "max", "stddev"]).describe("The aggregate function to compute"),
7
+ onField: z.string().describe("The field to compute the statistic on"),
8
+ groupBy: z.string().optional().describe("Field to group results by"),
9
+ where: z.string().optional().describe("SQL WHERE clause to filter features"),
10
+ });
11
+ export async function execute(client, input) {
12
+ const outStatisticFieldName = `${input.statisticType}_${input.onField}`;
13
+ const response = await client.queryFeatures({
14
+ serviceId: input.serviceId,
15
+ layerId: input.layerId,
16
+ where: input.where ?? "1=1",
17
+ outStatistics: [
18
+ {
19
+ statisticType: input.statisticType,
20
+ onStatisticField: input.onField,
21
+ outStatisticFieldName,
22
+ },
23
+ ],
24
+ groupByFieldsForStatistics: input.groupBy,
25
+ returnGeometry: false,
26
+ });
27
+ const features = response.features ?? [];
28
+ return jsonText({
29
+ statistics: features.map((f) => ({ attributes: f.attributes })),
30
+ });
31
+ }
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@honua/mcp-server",
3
+ "version": "0.0.6-alpha.0",
4
+ "description": "MCP server for Honua geospatial feature services",
5
+ "type": "module",
6
+ "bin": {
7
+ "honua-mcp": "dist/src/index.js"
8
+ },
9
+ "scripts": {
10
+ "clean": "rm -rf dist coverage",
11
+ "build": "npm run clean --silent && tsc -p tsconfig.json",
12
+ "prepack": "npm run build --silent",
13
+ "typecheck": "tsc --noEmit",
14
+ "test": "npm run build --silent && vitest run --coverage",
15
+ "test:unit": "vitest run",
16
+ "test:coverage": "vitest run --coverage",
17
+ "lint": "biome lint src/ test/",
18
+ "lint:fix": "biome lint --write src/ test/",
19
+ "format": "biome format src/ test/",
20
+ "format:fix": "biome format --write src/ test/",
21
+ "check": "biome check src/ test/",
22
+ "check:fix": "biome check --write src/ test/"
23
+ },
24
+ "dependencies": {
25
+ "@modelcontextprotocol/sdk": "^1.29.0",
26
+ "zod": "^3.24.0"
27
+ },
28
+ "peerDependencies": {
29
+ "@honua/sdk-js": "^0.0.6-alpha.0"
30
+ },
31
+ "devDependencies": {
32
+ "@honua/sdk-js": "file:..",
33
+ "@biomejs/biome": "1.9.4",
34
+ "@types/node": "^24.3.1",
35
+ "@vitest/coverage-v8": "^4.0.16",
36
+ "typescript": "^5.9.2",
37
+ "vitest": "^4.0.16"
38
+ },
39
+ "files": [
40
+ "dist/src",
41
+ "README.md"
42
+ ],
43
+ "engines": {
44
+ "node": ">=20.0.0"
45
+ }
46
+ }