@honua/mcp-server 0.1.0-beta.0 → 0.1.1-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.
package/README.md CHANGED
@@ -75,6 +75,48 @@ All read-only:
75
75
  - `honua_explain_capability_gap`
76
76
  - `honua_get_style`, `honua_apply_style_preset` — structured-unavailable on a plain FeatureServer
77
77
 
78
+ Hosts that use signed safe-agent plans can import
79
+ `@honua/mcp-server/agent-execution`. Its `createReadOnlyMcpAgentExecutor` binds
80
+ one named read tool to `@honua/sdk-js/agent-safety`'s exact approved-operation,
81
+ durable-audit, and signed-receipt path. It never enables wildcard dispatch or
82
+ mutation on the standalone server. The descriptor requires a bounded exact name
83
+ and a deterministic `countRows(result)` callback; missing or invalid row counts
84
+ fail instead of being reported as zero.
85
+
86
+ ### Optional natural-language map-plan tools
87
+
88
+ Embedded hosts with a map runtime, BYO-LLM callback, approval verifier, and
89
+ atomic approval-use store can opt into two additional tools:
90
+
91
+ - `proposeMapPlan` — compiles an instruction into an inspectable, content-addressed plan and never executes it.
92
+ - `executeMapPlan` — accepts that plan plus a signed approval envelope and executes only after scope, signature,
93
+ expiry, single-use, cancellation, and plan-identity checks. The envelope is required for effects and for every
94
+ read step that names a `sourceId`; only non-source inspection may use read-only auto-execution.
95
+
96
+ ```ts
97
+ import { createNlMapControlMcpHost, createServer } from "@honua/mcp-server";
98
+
99
+ const nlMapControl = createNlMapControlMcpHost({
100
+ control: {
101
+ tools: { runtime },
102
+ llm: callYourModel,
103
+ approvalVerifier,
104
+ receiptSigner,
105
+ },
106
+ approvalUseConsumer, // host-owned atomic consume + verify callbacks
107
+ resolveAuthorizationScopes: (transportScopes) => transportScopes,
108
+ });
109
+
110
+ const server = createServer(honuaClient, { nlMapControl });
111
+ ```
112
+
113
+ The callbacks are deliberately not inferred from environment variables: model
114
+ transport, map authority, signing keys, and replay storage remain host-owned.
115
+ The MCP response omits raw tool results and the original instruction from its
116
+ execution receipt. Plans containing credential, cursor, or endpoint material
117
+ are refused instead of returning a redacted plan that could no longer match its
118
+ approval fingerprint.
119
+
78
120
  ## MCP Resources
79
121
 
80
122
  - `honua://services`
@@ -112,9 +154,16 @@ npm run certify
112
154
 
113
155
  # CI entry points (also runnable locally):
114
156
  npm run test:certification # gate: runs harness tests + certifier, exits non-zero on failure
157
+ npm run test:certification:nl-map-control # gate: approval/scope/replay/cancellation/receipt security matrix
115
158
  npm run test:certification:artifact # evidence: writes artifacts, always exits 0
116
159
  ```
117
160
 
161
+ The certification test lane also hosts the optional NL tools over an in-memory
162
+ MCP transport with a deterministic fixture completion and mock map runtime. It proves
163
+ propose→approve→execute plus missing, expired, replayed, wrong-scope, tampered,
164
+ cancelled, deterministic-receipt, and redaction cases without a model, API key,
165
+ network service, or wall-clock dependency.
166
+
118
167
  ### Platform-free certification lane
119
168
 
120
169
  A dedicated **standalone** target (`HONUA_MCP_CERT_TARGET=standalone`, or
@@ -0,0 +1,16 @@
1
+ import type { AgentOperationExecutorV1 } from "@honua/sdk-js/agent-safety";
2
+ import type { JsonValue } from "@honua/sdk-js/query-planner";
3
+ /** A named, read-only MCP tool handler adapted to the SDK approval boundary. */
4
+ export interface ReadOnlyMcpAgentTool<TArguments> {
5
+ readonly name: string;
6
+ parse(parameters: JsonValue): TArguments;
7
+ execute(arguments_: TArguments, signal?: AbortSignal): Promise<unknown>;
8
+ /** Deterministically count logical rows/records in the returned MCP result. */
9
+ countRows(result: unknown): number;
10
+ }
11
+ /**
12
+ * Adapt a standalone MCP read tool without introducing a second approval or
13
+ * audit model. The SDK validates the exact tool/effect before this handler runs.
14
+ */
15
+ export declare function createReadOnlyMcpAgentExecutor<TArguments>(tool: ReadOnlyMcpAgentTool<TArguments>): AgentOperationExecutorV1;
16
+ //# sourceMappingURL=agent-execution.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent-execution.d.ts","sourceRoot":"","sources":["../../src/agent-execution.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAEV,wBAAwB,EAEzB,MAAM,4BAA4B,CAAC;AACpC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,6BAA6B,CAAC;AAE7D,gFAAgF;AAChF,MAAM,WAAW,oBAAoB,CAAC,UAAU;IAC9C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,KAAK,CAAC,UAAU,EAAE,SAAS,GAAG,UAAU,CAAC;IACzC,OAAO,CAAC,UAAU,EAAE,UAAU,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACxE,+EAA+E;IAC/E,SAAS,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM,CAAC;CACpC;AAED;;;GAGG;AACH,wBAAgB,8BAA8B,CAAC,UAAU,EACvD,IAAI,EAAE,oBAAoB,CAAC,UAAU,CAAC,GACrC,wBAAwB,CA0B1B"}
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Adapt a standalone MCP read tool without introducing a second approval or
3
+ * audit model. The SDK validates the exact tool/effect before this handler runs.
4
+ */
5
+ export function createReadOnlyMcpAgentExecutor(tool) {
6
+ if (!tool || typeof tool !== "object")
7
+ throw new TypeError("MCP tool descriptor is required");
8
+ const name = dataProperty(tool, "name");
9
+ if (typeof name !== "string" || !/^[A-Za-z][A-Za-z0-9_.:-]{0,127}$/.test(name))
10
+ throw new TypeError("MCP tool name must be a bounded exact identifier");
11
+ const parse = callback(tool, "parse");
12
+ const execute = callback(tool, "execute");
13
+ const countRows = callback(tool, "countRows");
14
+ return Object.freeze({
15
+ tool: name,
16
+ effect: "read",
17
+ async execute(operation, _limits, signal) {
18
+ if (operation.tool !== name || operation.effect !== "read")
19
+ throw new TypeError("approved operation does not match the read-only MCP tool");
20
+ const arguments_ = parse(operation.parameters);
21
+ const result = await execute(arguments_, signal);
22
+ const rows = countRows(result);
23
+ if (!Number.isSafeInteger(rows) || rows < 0)
24
+ throw new TypeError("MCP tool row count must be a non-negative safe integer");
25
+ return { rows, value: result };
26
+ },
27
+ });
28
+ }
29
+ function dataProperty(input, key) {
30
+ let descriptor;
31
+ try {
32
+ descriptor = Reflect.getOwnPropertyDescriptor(input, key);
33
+ }
34
+ catch {
35
+ throw new TypeError(`MCP tool ${key} could not be safely captured`);
36
+ }
37
+ if (!descriptor || descriptor.get || descriptor.set)
38
+ throw new TypeError(`MCP tool ${key} must be a data property`);
39
+ return descriptor.value;
40
+ }
41
+ function callback(input, key) {
42
+ const value = dataProperty(input, key);
43
+ if (typeof value !== "function")
44
+ throw new TypeError(`MCP tool ${key} callback is required`);
45
+ return value.bind(input);
46
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"census-fixture-client.d.ts","sourceRoot":"","sources":["../../../src/certification/census-fixture-client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,WAAW,EAKZ,MAAM,eAAe,CAAC;AACvB,OAAO,EAML,KAAK,SAAS,EACf,MAAM,kBAAkB,CAAC;AA4F1B,gEAAgE;AAChE,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS,EAAE,CAOjE;AAyFD,qFAAqF;AACrF,wBAAgB,6BAA6B,IAAI,WAAW,CAiE3D"}
1
+ {"version":3,"file":"census-fixture-client.d.ts","sourceRoot":"","sources":["../../../src/certification/census-fixture-client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAOjD,OAAO,EAML,KAAK,SAAS,EACf,MAAM,kBAAkB,CAAC;AA4F1B,gEAAgE;AAChE,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS,EAAE,CAOjE;AAyFD,qFAAqF;AACrF,wBAAgB,6BAA6B,IAAI,WAAW,CAiE3D"}
@@ -1 +1 @@
1
- {"version":3,"file":"fixture-client.d.ts","sourceRoot":"","sources":["../../../src/certification/fixture-client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,WAAW,EAKZ,MAAM,eAAe,CAAC;AAsDvB,mFAAmF;AACnF,wBAAgB,mBAAmB,IAAI,WAAW,CAkEjD"}
1
+ {"version":3,"file":"fixture-client.d.ts","sourceRoot":"","sources":["../../../src/certification/fixture-client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AA4DjD,mFAAmF;AACnF,wBAAgB,mBAAmB,IAAI,WAAW,CAkEjD"}
@@ -1,4 +1,4 @@
1
- import type { EsriGeometryType, EsriSpatialRel } from "@honua/sdk-js";
1
+ import type { EsriGeometryType, EsriSpatialRel } from "@honua/sdk-js/honua";
2
2
  export declare const GEOMETRY_TYPES: readonly ["esriGeometryPoint", "esriGeometryPolyline", "esriGeometryPolygon", "esriGeometryEnvelope", "esriGeometryMultipoint"];
3
3
  export declare function mapSpatialRel(rel: string | undefined): EsriSpatialRel | undefined;
4
4
  export declare function clampLimit(limit: number | undefined): number;
@@ -1 +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;;;;;;;;;GASG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,SAAS,CAWvE;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"}
1
+ {"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../../src/helpers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAM5E,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;;;;;;;;;GASG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,SAAS,CAWvE;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"}
@@ -2,6 +2,9 @@
2
2
  import { HonuaClient } from "@honua/sdk-js";
3
3
  import type { HonuaTransport } from "@honua/sdk-js";
4
4
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
+ import { type NlMapControlMcpHost } from "./nl-map-control.js";
6
+ export { createNlMapControlMcpHost, registerNlMapControlMcpTools, } from "./nl-map-control.js";
7
+ export type { CreateNlMapControlMcpHostOptions, McpNlMapExecutionResponse, McpNlMapPlanResponse, McpNlMapReceipt, NlMapControlMcpHost, NlMapControlMcpRequestContext, } from "./nl-map-control.js";
5
8
  export interface RuntimeOptions {
6
9
  baseUrl: string;
7
10
  apiKey: string | undefined;
@@ -9,6 +12,10 @@ export interface RuntimeOptions {
9
12
  timeoutMs: number;
10
13
  retryMaxRetries: number;
11
14
  }
15
+ export interface CreateServerOptions {
16
+ /** Optional BYO-LLM/map-runtime host for the experimental NL map-plan tools. */
17
+ readonly nlMapControl?: NlMapControlMcpHost;
18
+ }
12
19
  export declare const SERVER_VERSION: string;
13
20
  export declare function resolveRuntimeOptions(env: NodeJS.ProcessEnv): RuntimeOptions;
14
21
  export declare function createClientFromEnv(env?: NodeJS.ProcessEnv): HonuaClient;
@@ -33,7 +40,7 @@ export declare function createClientFromEnv(env?: NodeJS.ProcessEnv): HonuaClien
33
40
  * mirrored to stdio by `honua-mcp-proxy` ({@link runProxy}); it requires a Honua
34
41
  * deployment. Standalone is the front door; the proxy is the upgrade path.
35
42
  */
36
- export declare function createServer(client: HonuaClient): McpServer;
43
+ export declare function createServer(client: HonuaClient, options?: CreateServerOptions): McpServer;
37
44
  /**
38
45
  * Run the platform-free standalone server end-to-end over stdio.
39
46
  *
@@ -1 +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;AAgBtF,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;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,WAAW,aAyF/C;AAMD;;;;;;;;GAQG;AACH,wBAAsB,aAAa,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,OAAO,CAAC,IAAI,CAAC,CAKvF"}
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;AAGtF,OAAO,EAAE,KAAK,mBAAmB,EAAgC,MAAM,qBAAqB,CAAC;AAc7F,OAAO,EACL,yBAAyB,EACzB,4BAA4B,GAC7B,MAAM,qBAAqB,CAAC;AAC7B,YAAY,EACV,gCAAgC,EAChC,yBAAyB,EACzB,oBAAoB,EACpB,eAAe,EACf,mBAAmB,EACnB,6BAA6B,GAC9B,MAAM,qBAAqB,CAAC;AAE7B,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;AAED,MAAM,WAAW,mBAAmB;IAClC,gFAAgF;IAChF,QAAQ,CAAC,YAAY,CAAC,EAAE,mBAAmB,CAAC;CAC7C;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;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,WAAW,EAAE,OAAO,GAAE,mBAAwB,aA6FlF;AAMD;;;;;;;;GAQG;AACH,wBAAsB,aAAa,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,OAAO,CAAC,IAAI,CAAC,CAKvF"}
package/dist/src/index.js CHANGED
@@ -4,6 +4,7 @@ import { fileURLToPath } from "node:url";
4
4
  import { HonuaClient } from "@honua/sdk-js";
5
5
  import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
6
6
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
7
+ import { registerNlMapControlMcpTools } from "./nl-map-control.js";
7
8
  import * as layerSchemaResource from "./resources/layer-schema.js";
8
9
  import * as servicesResource from "./resources/services.js";
9
10
  import * as stylesResource from "./resources/styles.js";
@@ -16,6 +17,7 @@ import * as getStyle from "./tools/get-style.js";
16
17
  import * as listServices from "./tools/list-services.js";
17
18
  import * as queryFeatures from "./tools/query-features.js";
18
19
  import * as statistics from "./tools/statistics.js";
20
+ export { createNlMapControlMcpHost, registerNlMapControlMcpTools, } from "./nl-map-control.js";
19
21
  const DEFAULT_TIMEOUT_MS = 30_000;
20
22
  const DEFAULT_MAX_RETRIES = 2;
21
23
  const DEFAULT_SERVER_VERSION = "0.0.3-alpha.0";
@@ -137,13 +139,16 @@ export function createClientFromEnv(env = process.env) {
137
139
  * mirrored to stdio by `honua-mcp-proxy` ({@link runProxy}); it requires a Honua
138
140
  * deployment. Standalone is the front door; the proxy is the upgrade path.
139
141
  */
140
- export function createServer(client) {
142
+ export function createServer(client, options = {}) {
141
143
  const server = new McpServer({
142
144
  name: "honua",
143
145
  version: SERVER_VERSION,
144
146
  });
145
147
  // ── Tools ──────────────────────────────────────────────────────
146
148
  server.tool("honua_list_services", "Discover all available feature services on any FeatureServer folder. Set includeDetails=true for descriptions, layer counts, and spatial references. Degrades gracefully when the target has no service catalog.", listServices.schema.shape, async (args) => listServices.execute(client, listServices.schema.parse(args)));
149
+ if (options.nlMapControl) {
150
+ registerNlMapControlMcpTools(server, options.nlMapControl);
151
+ }
147
152
  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)));
148
153
  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)));
149
154
  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)));
@@ -0,0 +1,70 @@
1
+ import { type AgentApprovalUseConsumer } from "@honua/sdk-js/agent-safety";
2
+ import { type CreateNlMapControlOptions, NL_MAP_CONTROL_VERSION, type NlMapControl, type NlMapPlan, type NlMapPlanExecution } from "@honua/sdk-js/nl-map-control";
3
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
4
+ declare const MCP_NL_MAP_RECEIPT_KIND: "honua.mcp-nl-map-receipt";
5
+ export interface NlMapControlMcpRequestContext {
6
+ readonly signal?: AbortSignal;
7
+ /** Scopes authenticated by the MCP transport. Caller arguments never populate this field. */
8
+ readonly transportAuthorizationScopes?: readonly string[];
9
+ }
10
+ export interface CreateNlMapControlMcpHostOptions {
11
+ /** The identical controller configuration used by the in-app NL surface. */
12
+ readonly control: CreateNlMapControlOptions;
13
+ /** Atomic, host-authenticated approval-use store. Required for every approved MCP execution. */
14
+ readonly approvalUseConsumer: AgentApprovalUseConsumer;
15
+ /**
16
+ * Resolve trusted scopes for transports without MCP `authInfo` (for example an
17
+ * embedded stdio host). The input contains transport-authenticated scopes only.
18
+ */
19
+ readonly resolveAuthorizationScopes?: (transportScopes: readonly string[]) => readonly string[] | Promise<readonly string[]>;
20
+ }
21
+ export interface McpNlMapPlanResponse {
22
+ readonly plan: NlMapPlan;
23
+ readonly approvalRequired: boolean;
24
+ }
25
+ export interface McpNlMapReceipt {
26
+ readonly kind: typeof MCP_NL_MAP_RECEIPT_KIND;
27
+ readonly version: typeof NL_MAP_CONTROL_VERSION;
28
+ readonly id: string;
29
+ readonly planId: string;
30
+ readonly planFingerprint: `sha256:${string}`;
31
+ readonly mode: NlMapPlanExecution["mode"];
32
+ readonly outcome: NlMapPlanExecution["outcome"];
33
+ readonly approvalDigest?: `sha256:${string}`;
34
+ readonly startedAt: string;
35
+ readonly completedAt: string;
36
+ readonly steps: NlMapPlanExecution["receipt"]["steps"];
37
+ /** Digest of the SDK receipt retained by the host. Its raw instruction is never returned over MCP. */
38
+ readonly sdkReceiptDigest: `sha256:${string}`;
39
+ readonly receiptDigest: `sha256:${string}`;
40
+ }
41
+ export interface McpNlMapExecutionResponse {
42
+ readonly planId: string;
43
+ readonly planFingerprint: `sha256:${string}`;
44
+ readonly mode: NlMapPlanExecution["mode"];
45
+ readonly outcome: NlMapPlanExecution["outcome"];
46
+ readonly receipt: McpNlMapReceipt;
47
+ }
48
+ export interface NlMapControlMcpHost {
49
+ readonly control: NlMapControl;
50
+ propose(input: {
51
+ readonly instruction: string;
52
+ }, context?: NlMapControlMcpRequestContext): Promise<McpNlMapPlanResponse>;
53
+ execute(input: {
54
+ readonly plan: unknown;
55
+ readonly approval?: unknown;
56
+ }, context?: NlMapControlMcpRequestContext): Promise<McpNlMapExecutionResponse>;
57
+ }
58
+ /**
59
+ * Bind the experimental NL map controller to the MCP trust boundary.
60
+ *
61
+ * The host pre-authorizes and atomically consumes every approved step before
62
+ * delegating to `createNlMapControl`. This preserves the SDK's exact plan and
63
+ * signature checks while preventing approval replay through a second transport
64
+ * call. No caller-provided credential or cursor is included in MCP output.
65
+ */
66
+ export declare function createNlMapControlMcpHost(options: CreateNlMapControlMcpHostOptions): NlMapControlMcpHost;
67
+ /** Register `proposeMapPlan` and `executeMapPlan` on a real MCP server. */
68
+ export declare function registerNlMapControlMcpTools(server: McpServer, host: NlMapControlMcpHost): void;
69
+ export {};
70
+ //# sourceMappingURL=nl-map-control.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"nl-map-control.d.ts","sourceRoot":"","sources":["../../src/nl-map-control.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,470 @@
1
+ import { verifyAgentApproval, verifyAgentStepAuthorization, } from "@honua/sdk-js/agent-safety";
2
+ import { NL_MAP_CONTROL_TOOL_DEFINITIONS, NL_MAP_CONTROL_VERSION, NL_MAP_PLAN_KIND, agentEffectForNlEffect, createNlMapControl, hashNlMapPlan, } from "@honua/sdk-js/nl-map-control";
3
+ import { canonicalStringify, sha256, toJsonValue } from "@honua/sdk-js/query-planner";
4
+ import { z } from "zod";
5
+ import { snapshotMcpJson } from "./stable-json.js";
6
+ const MCP_NL_MAP_RECEIPT_KIND = "honua.mcp-nl-map-receipt";
7
+ const PLAN_ID = /^nlplan_[0-9a-f]{16}$/;
8
+ const DIGEST = /^sha256:[0-9a-f]{64}$/;
9
+ const SENSITIVE_KEYS = new Set([
10
+ "accesstoken",
11
+ "apikey",
12
+ "authorization",
13
+ "cookie",
14
+ "credential",
15
+ "credentials",
16
+ "cursor",
17
+ "endpoint",
18
+ "password",
19
+ "refreshtoken",
20
+ "resumetoken",
21
+ "secret",
22
+ "signedurl",
23
+ "token",
24
+ "url",
25
+ "uri",
26
+ "watermark",
27
+ ]);
28
+ const CREDENTIAL_TEXT = /\b(?:bearer|basic)\s+[A-Za-z0-9._~+/=-]+|\b(?:api[_-]?key|access[_-]?token|password|secret)\s*[:=]\s*[^\s,;]+/giu;
29
+ const CURSOR_TEXT = /\b(?:cursor|resume[_ -]?token|watermark)\s*[:=]\s*[^\s,;]+/giu;
30
+ const URL_TEXT = /https?:\/\/[^\s"'<>]+/giu;
31
+ const PUBLIC_ERROR_CODES = new Set([
32
+ "aborted",
33
+ "approval-expired",
34
+ "approval-invalid",
35
+ "approval-required",
36
+ "authorization-scope-denied",
37
+ "cancelled",
38
+ "context-mismatch",
39
+ "fixture-mismatch",
40
+ "integrity-failed",
41
+ "invalid-input",
42
+ "invalid-options",
43
+ "plan-invalid",
44
+ "plan-required",
45
+ "policy-denied",
46
+ "refusal",
47
+ "retries-exhausted",
48
+ "signature-invalid",
49
+ "unsafe-output",
50
+ ]);
51
+ class HonuaNlMapMcpError extends Error {
52
+ code;
53
+ constructor(code, message) {
54
+ super(message);
55
+ this.name = "HonuaNlMapMcpError";
56
+ this.code = code;
57
+ }
58
+ }
59
+ /**
60
+ * Bind the experimental NL map controller to the MCP trust boundary.
61
+ *
62
+ * The host pre-authorizes and atomically consumes every approved step before
63
+ * delegating to `createNlMapControl`. This preserves the SDK's exact plan and
64
+ * signature checks while preventing approval replay through a second transport
65
+ * call. No caller-provided credential or cursor is included in MCP output.
66
+ */
67
+ export function createNlMapControlMcpHost(options) {
68
+ if (!options?.control?.approvalVerifier) {
69
+ throw new TypeError("NL map MCP hosting requires control.approvalVerifier");
70
+ }
71
+ if (!options.approvalUseConsumer) {
72
+ throw new TypeError("NL map MCP hosting requires an atomic approvalUseConsumer");
73
+ }
74
+ const control = createNlMapControl(options.control);
75
+ const approvalVerifier = options.control.approvalVerifier;
76
+ const approvalUseConsumer = options.approvalUseConsumer;
77
+ const resolveAuthorizationScopes = options.resolveAuthorizationScopes;
78
+ const configuredNow = options.control.policy?.now;
79
+ const now = () => configuredNow?.() ?? new Date().toISOString();
80
+ return Object.freeze({
81
+ control,
82
+ async propose(input, context = {}) {
83
+ const signal = requestSignal(context);
84
+ signal?.throwIfAborted();
85
+ const request = snapshotMcpObject(input, "proposeMapPlan input");
86
+ const instruction = request.instruction;
87
+ if (typeof instruction !== "string" || instruction.trim().length === 0 || instruction.length > 16_384) {
88
+ throw new HonuaNlMapMcpError("invalid-input", "proposeMapPlan requires a non-empty instruction");
89
+ }
90
+ const plan = parseMcpPlan(await control.propose(instruction, {
91
+ ...(signal ? { signal } : {}),
92
+ }));
93
+ signal?.throwIfAborted();
94
+ assertMcpSafePlan(plan);
95
+ return Object.freeze({ plan, approvalRequired: planRequiresMcpApproval(plan) });
96
+ },
97
+ async execute(input, context = {}) {
98
+ const signal = requestSignal(context);
99
+ signal?.throwIfAborted();
100
+ const request = snapshotMcpObject(input, "executeMapPlan input");
101
+ const plan = parseMcpPlan(request.plan);
102
+ assertMcpSafePlan(plan);
103
+ const approval = request.approval === undefined ? undefined : request.approval;
104
+ if (approval === undefined && planRequiresMcpApproval(plan)) {
105
+ throw new HonuaNlMapMcpError("approval-required", "MCP source-scoped and effectful map plans require a signed approval envelope");
106
+ }
107
+ if (approval !== undefined) {
108
+ const authorizationTime = now();
109
+ await verifyAgentApproval(approval.dryRun, approval.policy, approval.approval, approvalVerifier, approval.context, {
110
+ now: authorizationTime,
111
+ ...(signal ? { signal } : {}),
112
+ });
113
+ signal?.throwIfAborted();
114
+ assertApprovalPlanIdentity(plan, approval);
115
+ const transportScopes = requestAuthorizationScopes(context);
116
+ const grantedInput = resolveAuthorizationScopes
117
+ ? await resolveAuthorizationScopes(transportScopes)
118
+ : transportScopes;
119
+ signal?.throwIfAborted();
120
+ const grantedScopes = snapshotAuthorizationScopes(grantedInput, "resolved authorization scopes");
121
+ assertAuthorizationScopes(approval, grantedScopes);
122
+ for (let index = 0; index < plan.steps.length; index += 1) {
123
+ signal?.throwIfAborted();
124
+ const step = plan.steps[index];
125
+ const approvedStep = approval.dryRun.plan.steps[index];
126
+ const sourceId = authorizationSourceId(step, approvedStep.source.id);
127
+ await verifyAgentStepAuthorization(approval.dryRun, approval.policy, approval.approval, approvalVerifier, approval.context, step.id, {
128
+ tool: step.tool,
129
+ effect: agentEffectForNlEffect(step.effect),
130
+ sourceId,
131
+ queryPlan: approvedStep.queryPlan,
132
+ fields: approvedStep.fields,
133
+ parameters: toJsonValue(step.call.args ?? {}),
134
+ }, approvalUseConsumer, {
135
+ now: authorizationTime,
136
+ ...(signal ? { signal } : {}),
137
+ });
138
+ }
139
+ }
140
+ signal?.throwIfAborted();
141
+ const execution = await control.execute(plan, {
142
+ ...(approval ? { approval } : {}),
143
+ ...(signal ? { signal } : {}),
144
+ });
145
+ return executionResponse(execution);
146
+ },
147
+ });
148
+ }
149
+ const proposeSchema = z
150
+ .object({
151
+ instruction: z.string().trim().min(1).max(16_384),
152
+ })
153
+ .strict();
154
+ const executeSchema = z
155
+ .object({
156
+ plan: z.record(z.string(), z.unknown()),
157
+ approval: z.record(z.string(), z.unknown()).optional(),
158
+ })
159
+ .strict();
160
+ /** Register `proposeMapPlan` and `executeMapPlan` on a real MCP server. */
161
+ export function registerNlMapControlMcpTools(server, host) {
162
+ const proposeDefinition = NL_MAP_CONTROL_TOOL_DEFINITIONS[0];
163
+ const executeDefinition = NL_MAP_CONTROL_TOOL_DEFINITIONS[1];
164
+ server.registerTool(proposeDefinition.name, {
165
+ title: proposeDefinition.title,
166
+ description: proposeDefinition.description,
167
+ inputSchema: proposeSchema,
168
+ annotations: {
169
+ readOnlyHint: true,
170
+ destructiveHint: false,
171
+ idempotentHint: false,
172
+ openWorldHint: true,
173
+ },
174
+ }, async (args, extra) => {
175
+ try {
176
+ return successResult(await host.propose(args, {
177
+ signal: extra.signal,
178
+ transportAuthorizationScopes: extra.authInfo?.scopes ?? [],
179
+ }));
180
+ }
181
+ catch (error) {
182
+ return errorResult(error, extra.signal);
183
+ }
184
+ });
185
+ server.registerTool(executeDefinition.name, {
186
+ title: executeDefinition.title,
187
+ description: `${executeDefinition.description} In the MCP host, source-scoped reads also require a signed approval so transport scopes can be matched to the exact source binding.`,
188
+ inputSchema: executeSchema,
189
+ annotations: {
190
+ readOnlyHint: false,
191
+ destructiveHint: true,
192
+ idempotentHint: false,
193
+ openWorldHint: false,
194
+ },
195
+ }, async (args, extra) => {
196
+ try {
197
+ return successResult(await host.execute({ plan: args.plan, ...(args.approval === undefined ? {} : { approval: args.approval }) }, {
198
+ signal: extra.signal,
199
+ transportAuthorizationScopes: extra.authInfo?.scopes ?? [],
200
+ }));
201
+ }
202
+ catch (error) {
203
+ return errorResult(error, extra.signal);
204
+ }
205
+ });
206
+ }
207
+ function snapshotMcpObject(input, label, code = "invalid-input") {
208
+ try {
209
+ const snapshot = snapshotMcpJson(input, label);
210
+ if (snapshot === null || typeof snapshot !== "object" || Array.isArray(snapshot)) {
211
+ throw new TypeError(`${label} must be an object`);
212
+ }
213
+ return snapshot;
214
+ }
215
+ catch {
216
+ throw new HonuaNlMapMcpError(code, `${label} is not safe bounded JSON`);
217
+ }
218
+ }
219
+ function requestSignal(context) {
220
+ const value = requestContextProperty(context, "signal");
221
+ if (value === undefined)
222
+ return undefined;
223
+ try {
224
+ if (!(value instanceof AbortSignal))
225
+ throw new TypeError("signal must be an AbortSignal");
226
+ }
227
+ catch {
228
+ throw new HonuaNlMapMcpError("invalid-input", "MCP request context contains an invalid signal");
229
+ }
230
+ return value;
231
+ }
232
+ function requestAuthorizationScopes(context) {
233
+ const value = requestContextProperty(context, "transportAuthorizationScopes");
234
+ return snapshotAuthorizationScopes(value ?? [], "transport authorization scopes");
235
+ }
236
+ function requestContextProperty(context, key) {
237
+ try {
238
+ if (!context || typeof context !== "object" || Array.isArray(context)) {
239
+ throw new TypeError("context must be an object");
240
+ }
241
+ const prototype = Object.getPrototypeOf(context);
242
+ if (prototype !== Object.prototype && prototype !== null)
243
+ throw new TypeError("context must be plain");
244
+ const descriptor = Reflect.getOwnPropertyDescriptor(context, key);
245
+ if (!descriptor)
246
+ return undefined;
247
+ if (!("value" in descriptor) || !descriptor.enumerable)
248
+ throw new TypeError("context property must be data");
249
+ return descriptor.value;
250
+ }
251
+ catch {
252
+ throw new HonuaNlMapMcpError("invalid-input", "MCP request context could not be read safely");
253
+ }
254
+ }
255
+ function snapshotAuthorizationScopes(input, label) {
256
+ let snapshot;
257
+ try {
258
+ snapshot = snapshotMcpJson(input, label);
259
+ }
260
+ catch {
261
+ throw new HonuaNlMapMcpError("invalid-input", `${label} are not safe bounded JSON`);
262
+ }
263
+ if (!Array.isArray(snapshot) ||
264
+ snapshot.length > 128 ||
265
+ snapshot.some((scope) => typeof scope !== "string" || scope.length === 0 || hasControlCharacters(scope))) {
266
+ throw new HonuaNlMapMcpError("invalid-input", `${label} must be a bounded list of non-empty strings`);
267
+ }
268
+ return Object.freeze([...new Set(snapshot)].sort());
269
+ }
270
+ function parseMcpPlan(input) {
271
+ const plan = snapshotMcpObject(input, "NL map plan");
272
+ if (plan.kind !== NL_MAP_PLAN_KIND ||
273
+ plan.version !== NL_MAP_CONTROL_VERSION ||
274
+ typeof plan.id !== "string" ||
275
+ !PLAN_ID.test(plan.id) ||
276
+ typeof plan.fingerprint !== "string" ||
277
+ !DIGEST.test(plan.fingerprint) ||
278
+ !Array.isArray(plan.steps) ||
279
+ plan.steps.length === 0) {
280
+ throw new HonuaNlMapMcpError("invalid-input", "executeMapPlan requires a valid NL map plan identity");
281
+ }
282
+ let fingerprint;
283
+ try {
284
+ fingerprint = hashNlMapPlan(plan);
285
+ }
286
+ catch {
287
+ throw new HonuaNlMapMcpError("invalid-input", "executeMapPlan plan content is not canonical JSON");
288
+ }
289
+ const expectedId = `nlplan_${fingerprint.slice("sha256:".length, "sha256:".length + 16)}`;
290
+ if (fingerprint !== plan.fingerprint || plan.id !== expectedId) {
291
+ throw new HonuaNlMapMcpError("invalid-input", "executeMapPlan plan identity does not match its content");
292
+ }
293
+ return plan;
294
+ }
295
+ function assertApprovalPlanIdentity(plan, approval) {
296
+ if (!approval ||
297
+ typeof approval !== "object" ||
298
+ !approval.dryRun ||
299
+ approval.dryRun.plan.id !== plan.id ||
300
+ approval.dryRun.plan.steps.length !== plan.steps.length) {
301
+ throw new HonuaNlMapMcpError("invalid-input", "approval does not bind to the supplied plan identity");
302
+ }
303
+ for (let index = 0; index < plan.steps.length; index += 1) {
304
+ const step = plan.steps[index];
305
+ const approved = approval.dryRun.plan.steps[index];
306
+ if (approved.id !== step.id ||
307
+ approved.tool !== step.tool ||
308
+ approved.effect !== agentEffectForNlEffect(step.effect) ||
309
+ approved.queryPlan.fingerprint !== plan.fingerprint) {
310
+ throw new HonuaNlMapMcpError("invalid-input", "approval step identity does not match the supplied plan");
311
+ }
312
+ authorizationSourceId(step, approved.source.id);
313
+ }
314
+ }
315
+ function authorizationSourceId(step, approvedSourceId) {
316
+ const sourceId = step.call.args?.sourceId;
317
+ if (sourceId !== undefined) {
318
+ if (typeof sourceId !== "string" || sourceId.length === 0 || sourceId !== approvedSourceId) {
319
+ throw new HonuaNlMapMcpError("invalid-input", "approval source binding does not match the supplied map operation");
320
+ }
321
+ return sourceId;
322
+ }
323
+ return approvedSourceId;
324
+ }
325
+ function planRequiresMcpApproval(plan) {
326
+ return (!plan.readOnly ||
327
+ plan.steps.some((step) => step.call.args?.sourceId !== undefined));
328
+ }
329
+ function assertAuthorizationScopes(approval, grantedInput) {
330
+ const granted = new Set(grantedInput);
331
+ const required = new Set(approval.dryRun.plan.steps.flatMap((step) => [...step.source.authorizationScope]));
332
+ for (const scope of required) {
333
+ if (!granted.has(scope)) {
334
+ throw new HonuaNlMapMcpError("authorization-scope-denied", "MCP transport authorization does not cover the approved map operation");
335
+ }
336
+ }
337
+ }
338
+ function assertMcpSafePlan(plan) {
339
+ assertMcpSafeJson(plan, "NL map plan");
340
+ }
341
+ function assertMcpSafeJson(value, label) {
342
+ let original;
343
+ let redacted;
344
+ try {
345
+ original = canonicalStringify(toJsonValue(value));
346
+ redacted = canonicalStringify(toJsonValue(redactForMcp(value)));
347
+ }
348
+ catch {
349
+ throw new HonuaNlMapMcpError("invalid-input", `${label} is not safe canonical JSON`);
350
+ }
351
+ if (original !== redacted) {
352
+ throw new HonuaNlMapMcpError("unsafe-output", `${label} contains credential, cursor, or endpoint material that cannot cross the MCP boundary`);
353
+ }
354
+ }
355
+ function executionResponse(execution) {
356
+ const sdkReceipt = execution.receipt;
357
+ const unsignedReceipt = snapshotMcpObject({
358
+ kind: MCP_NL_MAP_RECEIPT_KIND,
359
+ version: NL_MAP_CONTROL_VERSION,
360
+ planId: execution.planId,
361
+ planFingerprint: execution.planFingerprint,
362
+ mode: execution.mode,
363
+ outcome: execution.outcome,
364
+ ...(sdkReceipt.approvalDigest ? { approvalDigest: sdkReceipt.approvalDigest } : {}),
365
+ startedAt: sdkReceipt.startedAt,
366
+ completedAt: sdkReceipt.completedAt,
367
+ steps: sdkReceipt.steps,
368
+ sdkReceiptDigest: sdkReceipt.receiptDigest,
369
+ }, "MCP execution receipt", "unsafe-output");
370
+ assertMcpSafeJson(unsignedReceipt, "MCP execution receipt");
371
+ const receiptDigest = sha256(canonicalStringify(toJsonValue(unsignedReceipt)));
372
+ const receipt = snapshotMcpObject({
373
+ ...unsignedReceipt,
374
+ id: `mcpreceipt_${receiptDigest.slice("sha256:".length, "sha256:".length + 16)}`,
375
+ receiptDigest,
376
+ }, "MCP execution receipt", "unsafe-output");
377
+ const response = snapshotMcpObject({
378
+ planId: execution.planId,
379
+ planFingerprint: execution.planFingerprint,
380
+ mode: execution.mode,
381
+ outcome: execution.outcome,
382
+ receipt,
383
+ }, "MCP execution response", "unsafe-output");
384
+ assertMcpSafeJson(response, "MCP execution response");
385
+ return response;
386
+ }
387
+ function successResult(value) {
388
+ return {
389
+ content: [{ type: "text", text: JSON.stringify(value) }],
390
+ };
391
+ }
392
+ function errorResult(error, signal) {
393
+ const code = signal.aborted ? "cancelled" : safeErrorCode(error);
394
+ const message = safeErrorMessage(code);
395
+ return {
396
+ isError: true,
397
+ content: [{ type: "text", text: JSON.stringify({ error: { code, message } }) }],
398
+ };
399
+ }
400
+ function safeErrorCode(error) {
401
+ try {
402
+ if (error && typeof error === "object") {
403
+ const descriptor = Reflect.getOwnPropertyDescriptor(error, "code");
404
+ const code = descriptor && "value" in descriptor ? descriptor.value : undefined;
405
+ if (typeof code === "string" && PUBLIC_ERROR_CODES.has(code))
406
+ return code;
407
+ }
408
+ }
409
+ catch {
410
+ return "execution-refused";
411
+ }
412
+ return "execution-refused";
413
+ }
414
+ function safeErrorMessage(code) {
415
+ switch (code) {
416
+ case "cancelled":
417
+ case "aborted":
418
+ return "The map-plan request was cancelled before execution.";
419
+ case "authorization-scope-denied":
420
+ return "The authenticated MCP scopes do not authorize this map plan.";
421
+ case "approval-required":
422
+ return "This map plan requires a signed approval envelope.";
423
+ case "approval-expired":
424
+ return "The map-plan approval has expired.";
425
+ case "policy-denied":
426
+ return "The map-plan approval was denied or has already been used.";
427
+ case "signature-invalid":
428
+ case "integrity-failed":
429
+ case "context-mismatch":
430
+ case "approval-invalid":
431
+ return "The map-plan approval could not be verified.";
432
+ case "plan-invalid":
433
+ case "plan-required":
434
+ case "invalid-input":
435
+ return "The supplied map plan or approval is invalid.";
436
+ case "unsafe-output":
437
+ return "The plan was refused because its output would expose sensitive material.";
438
+ default:
439
+ return "The map-plan request was refused safely.";
440
+ }
441
+ }
442
+ function redactForMcp(value, key) {
443
+ if (key && SENSITIVE_KEYS.has(normalizeKey(key)))
444
+ return "[REDACTED]";
445
+ if (typeof value === "string")
446
+ return redactText(value);
447
+ if (Array.isArray(value))
448
+ return value.map((item) => redactForMcp(item));
449
+ if (value && typeof value === "object") {
450
+ return Object.fromEntries(Object.entries(value).map(([name, child]) => [name, redactForMcp(child, name)]));
451
+ }
452
+ return value;
453
+ }
454
+ function normalizeKey(value) {
455
+ return value.toLowerCase().replace(/[^a-z0-9]/gu, "");
456
+ }
457
+ function hasControlCharacters(value) {
458
+ for (let index = 0; index < value.length; index += 1) {
459
+ const code = value.charCodeAt(index);
460
+ if (code <= 31 || code === 127)
461
+ return true;
462
+ }
463
+ return false;
464
+ }
465
+ function redactText(value) {
466
+ return value
467
+ .replace(URL_TEXT, "[REDACTED_URL]")
468
+ .replace(CREDENTIAL_TEXT, "[REDACTED]")
469
+ .replace(CURSOR_TEXT, "[REDACTED]");
470
+ }
@@ -0,0 +1,14 @@
1
+ import type { JsonValue } from "@honua/sdk-js/query-planner";
2
+ /** Stable boundary error whose message never includes a reflected value or trap error. */
3
+ export declare class McpJsonSnapshotError extends Error {
4
+ constructor(message: string);
5
+ }
6
+ /**
7
+ * Detach untrusted JSON without invoking property getters.
8
+ *
9
+ * The clone is deeply frozen and bounded before any asynchronous authorization
10
+ * callback can observe it. Reflection failures (including Proxy traps) collapse
11
+ * to one stable error instead of reflecting attacker-controlled messages.
12
+ */
13
+ export declare function snapshotMcpJson(input: unknown, label: string): JsonValue;
14
+ //# sourceMappingURL=stable-json.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stable-json.d.ts","sourceRoot":"","sources":["../../src/stable-json.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,6BAA6B,CAAC;AAe7D,0FAA0F;AAC1F,qBAAa,oBAAqB,SAAQ,KAAK;gBAC1B,OAAO,EAAE,MAAM;CAKnC;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,GAAG,SAAS,CAOxE"}
@@ -0,0 +1,126 @@
1
+ const MAX_NODES = 8_192;
2
+ const MAX_DEPTH = 32;
3
+ const MAX_BYTES = 1_048_576;
4
+ const MAX_OBJECT_PROPERTIES = 128;
5
+ const MAX_ARRAY_LENGTH = 1_024;
6
+ const MAX_STRING_CODE_UNITS = 65_536;
7
+ const SNAPSHOT_ERRORS = new WeakSet();
8
+ /** Stable boundary error whose message never includes a reflected value or trap error. */
9
+ export class McpJsonSnapshotError extends Error {
10
+ constructor(message) {
11
+ super(message);
12
+ this.name = "McpJsonSnapshotError";
13
+ SNAPSHOT_ERRORS.add(this);
14
+ }
15
+ }
16
+ /**
17
+ * Detach untrusted JSON without invoking property getters.
18
+ *
19
+ * The clone is deeply frozen and bounded before any asynchronous authorization
20
+ * callback can observe it. Reflection failures (including Proxy traps) collapse
21
+ * to one stable error instead of reflecting attacker-controlled messages.
22
+ */
23
+ export function snapshotMcpJson(input, label) {
24
+ try {
25
+ return snapshot(input, 0, new WeakSet(), { nodes: 0, bytes: 0 });
26
+ }
27
+ catch (error) {
28
+ if (error !== null && typeof error === "object" && SNAPSHOT_ERRORS.has(error))
29
+ throw error;
30
+ throw new McpJsonSnapshotError(`${label} could not be safely snapshotted`);
31
+ }
32
+ }
33
+ function snapshot(value, depth, ancestors, budget) {
34
+ budget.nodes += 1;
35
+ if (budget.nodes > MAX_NODES)
36
+ invalid("MCP JSON exceeds the node limit");
37
+ if (depth > MAX_DEPTH)
38
+ invalid("MCP JSON exceeds the depth limit");
39
+ addBytes(budget, 8);
40
+ if (value === null || typeof value === "boolean")
41
+ return value;
42
+ if (typeof value === "string") {
43
+ if (value.length > MAX_STRING_CODE_UNITS)
44
+ invalid("MCP JSON contains an oversized string");
45
+ addBytes(budget, utf8Bytes(value));
46
+ return value;
47
+ }
48
+ if (typeof value === "number") {
49
+ if (!Number.isFinite(value))
50
+ invalid("MCP JSON contains a non-finite number");
51
+ return Object.is(value, -0) ? 0 : value;
52
+ }
53
+ if (typeof value !== "object")
54
+ invalid("MCP JSON contains an unsupported value");
55
+ if (ancestors.has(value))
56
+ invalid("MCP JSON must not contain cycles");
57
+ ancestors.add(value);
58
+ const result = Array.isArray(value)
59
+ ? snapshotArray(value, depth, ancestors, budget)
60
+ : snapshotObject(value, depth, ancestors, budget);
61
+ ancestors.delete(value);
62
+ return result;
63
+ }
64
+ function snapshotArray(input, depth, ancestors, budget) {
65
+ if (Object.getPrototypeOf(input) !== Array.prototype)
66
+ invalid("MCP JSON arrays must use the standard prototype");
67
+ const lengthDescriptor = Reflect.getOwnPropertyDescriptor(input, "length");
68
+ const length = lengthDescriptor && "value" in lengthDescriptor ? lengthDescriptor.value : undefined;
69
+ if (!Number.isSafeInteger(length) || length < 0 || length > MAX_ARRAY_LENGTH) {
70
+ invalid("MCP JSON contains an invalid or oversized array");
71
+ }
72
+ const keys = Reflect.ownKeys(input);
73
+ if (keys.some((key) => typeof key !== "string"))
74
+ invalid("MCP JSON must not contain symbol keys");
75
+ if (keys.length !== length + 1 || !keys.includes("length")) {
76
+ invalid("MCP JSON arrays must be dense and must not contain extra properties");
77
+ }
78
+ const output = [];
79
+ for (let index = 0; index < length; index += 1) {
80
+ const descriptor = Reflect.getOwnPropertyDescriptor(input, String(index));
81
+ if (!descriptor || !("value" in descriptor) || !descriptor.enumerable) {
82
+ invalid("MCP JSON arrays must contain enumerable data properties");
83
+ }
84
+ output.push(snapshot(descriptor.value, depth + 1, ancestors, budget));
85
+ }
86
+ return Object.freeze(output);
87
+ }
88
+ function snapshotObject(input, depth, ancestors, budget) {
89
+ const prototype = Object.getPrototypeOf(input);
90
+ if (prototype !== Object.prototype && prototype !== null)
91
+ invalid("MCP JSON values must be plain objects");
92
+ const keys = Reflect.ownKeys(input);
93
+ if (keys.length > MAX_OBJECT_PROPERTIES)
94
+ invalid("MCP JSON object exceeds the property limit");
95
+ if (keys.some((key) => typeof key !== "string"))
96
+ invalid("MCP JSON must not contain symbol keys");
97
+ const output = Object.create(null);
98
+ for (const key of keys.sort()) {
99
+ if (key.length > MAX_STRING_CODE_UNITS)
100
+ invalid("MCP JSON contains an oversized property name");
101
+ addBytes(budget, utf8Bytes(key));
102
+ const descriptor = Reflect.getOwnPropertyDescriptor(input, key);
103
+ if (!descriptor || !("value" in descriptor) || !descriptor.enumerable) {
104
+ invalid("MCP JSON objects must contain enumerable data properties");
105
+ }
106
+ Object.defineProperty(output, key, {
107
+ value: snapshot(descriptor.value, depth + 1, ancestors, budget),
108
+ enumerable: true,
109
+ writable: false,
110
+ configurable: false,
111
+ });
112
+ }
113
+ return Object.freeze(output);
114
+ }
115
+ function addBytes(budget, increment) {
116
+ const next = budget.bytes + increment;
117
+ if (!Number.isSafeInteger(next) || next > MAX_BYTES)
118
+ invalid("MCP JSON exceeds the byte limit");
119
+ budget.bytes = next;
120
+ }
121
+ function utf8Bytes(value) {
122
+ return new TextEncoder().encode(value).byteLength;
123
+ }
124
+ function invalid(message) {
125
+ throw new McpJsonSnapshotError(message);
126
+ }
@@ -1,4 +1,4 @@
1
- import { type Capability, type Protocol } from "@honua/sdk-js";
1
+ import type { Capability, Protocol } from "@honua/sdk-js";
2
2
  import { z } from "zod";
3
3
  export declare const schema: z.ZodObject<{
4
4
  capability: z.ZodEffects<z.ZodString, Capability, string>;
@@ -7,13 +7,13 @@ export declare const schema: z.ZodObject<{
7
7
  declaredCapabilities: z.ZodOptional<z.ZodArray<z.ZodEffects<z.ZodString, Capability, string>, "many">>;
8
8
  }, "strip", z.ZodTypeAny, {
9
9
  capability: Capability;
10
- protocol?: Protocol | undefined;
11
10
  sourceId?: string | undefined;
11
+ protocol?: Protocol | undefined;
12
12
  declaredCapabilities?: Capability[] | undefined;
13
13
  }, {
14
14
  capability: string;
15
- protocol?: string | undefined;
16
15
  sourceId?: string | undefined;
16
+ protocol?: string | undefined;
17
17
  declaredCapabilities?: string[] | undefined;
18
18
  }>;
19
19
  export type Input = z.infer<typeof schema>;
@@ -1 +1 @@
1
- {"version":3,"file":"explain-capability-gap.d.ts","sourceRoot":"","sources":["../../../src/tools/explain-capability-gap.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,KAAK,UAAU,EAAa,KAAK,QAAQ,EAAE,MAAM,eAAe,CAAC;AAExF,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAaxB,eAAO,MAAM,MAAM;;;;;;;;;;;;;;;EAKjB,CAAC;AAEH,MAAM,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,MAAM,CAAC,CAAC;AAE3C,wBAAsB,OAAO,CAAC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK;;;;;GAI3D"}
1
+ {"version":3,"file":"explain-capability-gap.d.ts","sourceRoot":"","sources":["../../../src/tools/explain-capability-gap.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAG1D,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAaxB,eAAO,MAAM,MAAM;;;;;;;;;;;;;;;EAKjB,CAAC;AAEH,MAAM,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,MAAM,CAAC,CAAC;AAE3C,wBAAsB,OAAO,CAAC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK;;;;;GAI3D"}
@@ -1,5 +1,5 @@
1
- import { CAPABILITIES, PROTOCOLS } from "@honua/sdk-js";
2
1
  import { explainHonuaCapabilityGap } from "@honua/sdk-js/agent-tools";
2
+ import { CAPABILITIES, PROTOCOLS } from "@honua/sdk-js/contract";
3
3
  import { z } from "zod";
4
4
  import { jsonText } from "../helpers.js";
5
5
  const capabilitySchema = z.string().refine((value) => CAPABILITIES.includes(value), {
package/package.json CHANGED
@@ -1,12 +1,27 @@
1
1
  {
2
2
  "name": "@honua/mcp-server",
3
- "version": "0.1.0-beta.0",
3
+ "version": "0.1.1-beta.0",
4
4
  "description": "MCP server for Honua geospatial feature services",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "honua-mcp": "dist/src/index.js",
8
8
  "honua-mcp-proxy": "dist/src/proxy.js"
9
9
  },
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/src/index.d.ts",
13
+ "default": "./dist/src/index.js"
14
+ },
15
+ "./agent-execution": {
16
+ "types": "./dist/src/agent-execution.d.ts",
17
+ "default": "./dist/src/agent-execution.js"
18
+ },
19
+ "./nl-map-control": {
20
+ "types": "./dist/src/nl-map-control.d.ts",
21
+ "default": "./dist/src/nl-map-control.js"
22
+ },
23
+ "./package.json": "./package.json"
24
+ },
10
25
  "scripts": {
11
26
  "clean": "rm -rf dist coverage",
12
27
  "build": "npm run clean --silent && tsc -p tsconfig.json",
@@ -17,6 +32,7 @@
17
32
  "test:coverage": "vitest run --coverage",
18
33
  "certify": "npm run build --silent && node dist/src/certification/cli.js",
19
34
  "certify:standalone": "npm run build --silent && node dist/src/certification/cli.js --target standalone",
35
+ "test:certification:nl-map-control": "vitest run test/certification/nl-map-control.test.ts",
20
36
  "test:certification": "npm run build --silent && vitest run test/certification && node dist/src/certification/cli.js",
21
37
  "test:certification:artifact": "npm run build --silent && node dist/src/certification/cli.js --artifact-only",
22
38
  "test:certification:standalone": "npm run build --silent && node dist/src/certification/cli.js --target standalone",
@@ -44,7 +60,7 @@
44
60
  "hono": "^4.12.21"
45
61
  },
46
62
  "peerDependencies": {
47
- "@honua/sdk-js": "^0.1.0-beta.0"
63
+ "@honua/sdk-js": "^0.1.1-beta.0"
48
64
  },
49
65
  "devDependencies": {
50
66
  "@biomejs/biome": "1.9.4",