@honua/mcp-server 0.1.11-beta.0 → 0.1.13

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
@@ -8,6 +8,9 @@ bound to their own platform; this one is bound to none.
8
8
 
9
9
  **Release status: beta.** Tool contracts are certified against live and fixture
10
10
  targets on every release; remaining pre-1.0 work is hardening, not surface change.
11
+ "Beta" is a maturity label, not part of the version: releases are stable `0.x`
12
+ versions, and the `@honua/sdk-js` peer is a caret range (for example `^0.1.12`),
13
+ so a later `@honua/sdk-js` patch installs beside this package without a republish.
11
14
 
12
15
  **Scores are published, not claimed.** How well different client models actually
13
16
  drive this surface — and where they fail — is in the
@@ -253,9 +256,34 @@ atomic approval-use store can opt into two additional tools:
253
256
  expiry, single-use, cancellation, and plan-identity checks. The envelope is required for effects and for every
254
257
  read step that names a `sourceId`; only non-source inspection may use read-only auto-execution.
255
258
 
256
- ```ts
259
+ Embedding the server in your own Node.js host needs the packages as local
260
+ dependencies of that host project:
261
+
262
+ ```bash
263
+ npm install @honua/mcp-server @honua/sdk-js
264
+ ```
265
+
266
+ The map runtime, model callback, approval verifier, receipt signer, and
267
+ approval-use store are yours to supply; the `declare const` lines stand in for them:
268
+
269
+ ```ts doc-test=compile
270
+ import { HonuaClient } from "@honua/sdk-js";
271
+ import type {
272
+ AgentApprovalUseConsumer,
273
+ AgentEnvelopeSigner,
274
+ AgentEnvelopeVerifier,
275
+ } from "@honua/sdk-js/agent-safety";
276
+ import type { CreateNlMapControlOptions, NlMapControlToolsOptions } from "@honua/sdk-js/nl-map-control";
257
277
  import { createNlMapControlMcpHost, createServer } from "@honua/mcp-server";
258
278
 
279
+ declare const runtime: NlMapControlToolsOptions["runtime"];
280
+ declare const callYourModel: CreateNlMapControlOptions["llm"];
281
+ declare const approvalVerifier: AgentEnvelopeVerifier;
282
+ declare const receiptSigner: AgentEnvelopeSigner;
283
+ declare const approvalUseConsumer: AgentApprovalUseConsumer;
284
+
285
+ const honuaClient = new HonuaClient({ baseUrl: "https://services.arcgis.com/P3ePLMYs2RVChkJx/arcgis" });
286
+
259
287
  const nlMapControl = createNlMapControlMcpHost({
260
288
  control: {
261
289
  tools: { runtime },
@@ -308,6 +336,24 @@ For each tool the server advertises (over an in-memory MCP transport) it:
308
336
  It emits a stable machine-readable JSON report plus a human-readable Markdown
309
337
  summary, and exits non-zero on any conformance/round-trip failure.
310
338
 
339
+ The certification and eval scripts in this section and the next run from a clone
340
+ of this repository (Node.js `>=20.19` and git). `mcp/` links to the SDK at the
341
+ repository root, so build the SDK first:
342
+
343
+ ```bash
344
+ git clone --depth 1 https://github.com/honua-io/honua-sdk-js.git
345
+ cd honua-sdk-js
346
+ npm ci
347
+ ```
348
+
349
+ ```bash
350
+ npm run build # the SDK that mcp/ links to; takes a few minutes
351
+ cd mcp
352
+ npm ci
353
+ ```
354
+
355
+ From `mcp/`:
356
+
311
357
  ```bash
312
358
  # Run the certifier against the offline fixture backend and write artifacts:
313
359
  npm run certify
@@ -333,9 +379,13 @@ apportionment layer from `services.arcgis.com`, with **no** Honua surfaces. It
333
379
  proves the tools certify **green with honest skips** against a non-Honua endpoint:
334
380
  the data tools round-trip against real recorded data, the Honua-only style tools
335
381
  degrade to structured "not available" results, and the auth/mutation/job contracts
336
- skip-with-reason. Fixture-backed and deterministic — no network.
382
+ skip-with-reason. Fixture-backed and deterministic — no network — as long as
383
+ `HONUA_BASE_URL` is unset: when it is set, this lane certifies that live endpoint
384
+ instead (the scheduled live variant). Unset it if you exported it for `honua-mcp`
385
+ above:
337
386
 
338
387
  ```bash
388
+ unset HONUA_BASE_URL
339
389
  npm run certify:standalone # platform-free cert (plain FeatureServer fixture)
340
390
  npm run test:certification:standalone # CI gate variant
341
391
  ```
@@ -387,8 +437,8 @@ The honua server exposes one MCP catalog over streamable-HTTP/SSE at `/mcp`.
387
437
  Claude-Desktop-style clients speak **stdio**. Rather than reimplement that
388
438
  catalog (which is how the HTTP and stdio surfaces historically drifted apart),
389
439
  this package ships a **stdio proxy** (`honua-mcp-proxy`) that bridges a local
390
- stdio MCP client to the remote HTTP-SSE MCP server. It connects upstream as an
391
- MCP client and re-exposes the *same* catalog downstream over stdio — identical
440
+ stdio MCP client to the remote HTTP-SSE MCP server. It forwards MCP messages
441
+ between the transports and exposes the *same* catalog downstream over stdio — identical
392
442
  tools, identical input/output schemas, identical resources and prompts, and live
393
443
  `tools/list_changed` notifications. There is one source-of-truth catalog (the
394
444
  server's `/mcp`); the SDK proxies it, so the two transports are symmetric by
@@ -414,8 +464,33 @@ or `[::1]` HTTP development endpoints. Userinfo, query parameters, fragments,
414
464
  non-HTTP(S) URLs are rejected before connect, and redirect targets are never
415
465
  followed or sent credentials.
416
466
 
417
- A parity test (`test/proxy.test.ts`) asserts the tool/resource/template catalog
418
- the downstream client sees is byte-identical to the upstream surface, that
467
+ The proxy forwards the caller's `initialize` request before opening an initialized
468
+ upstream session, including `_meta["honua.io/workflow-view"]`. That HTTP session
469
+ is retained for subsequent requests, so initializing with `setup` makes a later
470
+ selector-free `tools/list` return the server's setup catalog. A temporary
471
+ per-request catalog override leaves the initialized view intact; separate proxy
472
+ processes have separate sessions.
473
+
474
+ The view selector is the only `initialize` metadata the server reads, and the
475
+ only metadata the proxy forwards: other `_meta` keys stay local rather than
476
+ entering the credentialed upstream session. The handshake fails closed. A
477
+ request before `initialize`, an `initialize` the server's selector contract
478
+ would refuse (a non-string or longer-than-64-character view), or a second
479
+ `initialize` is answered with JSON-RPC `-32600` without reaching the server,
480
+ and the proxy exits. If `initialize` does not arrive, or the server does not
481
+ answer it, within 30 seconds, the proxy exits as well.
482
+
483
+ Executable protocol tests (`test/proxy-session.test.ts`) exercise setup/default
484
+ initialization, concurrent sessions, authenticated full-catalog override and
485
+ restoration, `tools/list_changed` relay, and the fail-closed handshake against a
486
+ loopback fixture. The `MCP session contract` workflow also runs them against the
487
+ `honua-mcp-proxy` executable npm installs from the packed tarballs. The
488
+ published 0.1.12 reproduction and exact JSON wire captures are under
489
+ `test/fixtures/initialize-view/`.
490
+
491
+ A parity test (`test/proxy.test.ts`) runs the same bridge against the canonical
492
+ server over streamable HTTP. It asserts the tool/resource/template catalog the
493
+ downstream client sees is byte-identical to a direct HTTP client's, that
419
494
  `tools/call` and resource reads round-trip identically, and that `list_changed`
420
495
  notifications are forwarded.
421
496
 
@@ -524,6 +599,12 @@ Shared env for every live run:
524
599
  Set exactly one of `HONUA_MCP_AUTH_TOKEN` or `HONUA_API_KEY` for authenticated
525
600
  live runs; ambiguous authentication fails closed.
526
601
 
602
+ The commands below read three values you supply from your own accounts: `HONUA_TOKEN`
603
+ (a bearer token your operator deployment issued), and the model provider's
604
+ `ANTHROPIC_API_KEY` or `OPENAI_API_KEY` (the Bedrock driver uses the AWS credential
605
+ chain instead). Export the ones your driver needs before you run it, and run from `mcp/`:
606
+
607
+ <!-- doc-run: skip reason="billable live-model lane: needs the reader's own operator token and Anthropic, OpenAI or AWS model credentials; the offline control above (npm run eval:offline) is the runnable lane" -->
527
608
  ```bash
528
609
  # Anthropic Claude (default claude-opus-4-8; override with HONUA_EVAL_ANTHROPIC_MODEL):
529
610
  HONUA_MCP_REMOTE_URL="https://demo.honua.io/mcp" \
@@ -4,7 +4,7 @@ import { ADMIN_MCP_CONTRACT_SERVER_SHA, ADMIN_MCP_COVERAGE_SHA256, ADMIN_MCP_EXC
4
4
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
5
5
  import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
6
6
  import { isMainEntrypoint } from "../entrypoint.js";
7
- import { connectUpstream, createProxyServer, resolveProxyOptions } from "../proxy.js";
7
+ import { connectProxyTransport, connectUpstream, resolveProxyOptions } from "../proxy.js";
8
8
  const DEFAULT_EXPECTATION = {
9
9
  publishedToolNames: ADMIN_MCP_PUBLISHED_TOOL_NAMES,
10
10
  excludedToolNames: ADMIN_MCP_EXCLUDED_OPERATIONS.map((operation) => operation.toolName),
@@ -202,12 +202,13 @@ export async function runAdminParityCertification(env = process.env, outputPath
202
202
  if (ADMIN_MCP_CONTRACT_SERVER_SHA === null) {
203
203
  throw new Error("Admin MCP live certification is blocked until config/admin-client.v1.json pins the final server head.");
204
204
  }
205
- const upstream = await connectUpstream(resolveProxyOptions(env));
206
- const proxy = createProxyServer(upstream);
205
+ const options = resolveProxyOptions(env);
206
+ const upstream = await connectUpstream(options);
207
+ // The proxied side runs through the same bridge the stdio executable uses.
207
208
  const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
208
209
  const proxied = new Client({ name: "honua-admin-parity", version: "1.0.0" });
209
210
  try {
210
- await proxy.connect(serverTransport);
211
+ await connectProxyTransport(options, serverTransport);
211
212
  await proxied.connect(clientTransport);
212
213
  const directTools = await listAllTools(upstream);
213
214
  const proxiedTools = await listAllTools(proxied);
@@ -219,7 +220,6 @@ export async function runAdminParityCertification(env = process.env, outputPath
219
220
  }
220
221
  finally {
221
222
  await proxied.close().catch(() => { });
222
- await proxy.close().catch(() => { });
223
223
  await upstream.close().catch(() => { });
224
224
  }
225
225
  }
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
3
- import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3
+ import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
4
+ import { type InitializeRequest } from "@modelcontextprotocol/sdk/types.js";
4
5
  /**
5
6
  * Transport-symmetric stdio proxy for the honua MCP surface (honua-server #1950).
6
7
  *
@@ -8,10 +9,12 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
8
9
  * `/mcp`. Claude-Desktop-style clients speak stdio. Rather than reimplementing
9
10
  * the tool/resource catalog (the older `@honua/mcp-server` discovery surface did
10
11
  * exactly that, which is how the two halves drifted apart), this proxy bridges a
11
- * local stdio MCP client to the remote HTTP-SSE MCP server: it connects upstream
12
- * as an MCP client, then re-exposes the *same* catalog downstream over stdio.
12
+ * local stdio MCP client to the remote HTTP-SSE MCP server: each stdio client
13
+ * gets one upstream streamable-HTTP session, opened by the client's own
14
+ * initialize, and every later message is relayed over that session.
13
15
  *
14
- * Because every request and notification is forwarded verbatim, the stdio
16
+ * Because every request and notification is forwarded verbatim (initialize's
17
+ * `_meta` is narrowed to the workflow-view selector), the stdio
15
18
  * surface is transport-symmetric with the HTTP-SSE surface by construction —
16
19
  * identical tools, identical input/output schemas, identical resources and
17
20
  * prompts, and live `list_changed` notifications. There is one source-of-truth
@@ -28,19 +31,32 @@ export interface ProxyOptions {
28
31
  export declare function resolveProxyOptions(env?: NodeJS.ProcessEnv): ProxyOptions;
29
32
  /** Build request headers for the upstream connection from the resolved options. */
30
33
  export declare function buildUpstreamHeaders(options: ProxyOptions): Record<string, string>;
31
- /** Connect an upstream MCP client to the remote honua /mcp over streamable HTTP. */
32
- export declare function connectUpstream(options: ProxyOptions): Promise<Client>;
33
34
  /**
34
- * Build a low-level MCP server that forwards every request and `list_changed`
35
- * notification to the already-connected upstream client. The proxy advertises
36
- * exactly the upstream's name, version, and capabilities, so the downstream
37
- * (stdio) surface mirrors the upstream (HTTP-SSE) surface.
35
+ * Connect a direct MCP client to the remote honua /mcp over streamable HTTP.
36
+ * Certification and evals use it as the HTTP reference side; the stdio proxy
37
+ * does not, because Client.connect would send its own initialize.
38
38
  */
39
- export declare function createProxyServer(upstream: Client): Server;
39
+ export declare function connectUpstream(options: ProxyOptions): Promise<Client>;
40
+ /** The server's only initialize metadata key; the session view selector. */
41
+ export declare const WORKFLOW_VIEW_META_KEY = "honua.io/workflow-view";
42
+ /** Read the optional view selector under the server's own contract. */
43
+ export declare function readWorkflowView(request: InitializeRequest): string | undefined;
44
+ export interface ProxyBridgeOptions {
45
+ /** Bound on initialize arriving and its upstream response; defaults to 30 s. */
46
+ initializeTimeoutMs?: number;
47
+ }
40
48
  /**
41
- * Run the stdio proxy end-to-end: resolve the remote `/mcp` from the
42
- * environment, connect upstream, and expose the mirrored catalog over stdio.
43
- * Also used by the deprecated `honua-mcp` bin, which now delegates here.
49
+ * Bridge one downstream transport to one upstream HTTP session. In particular,
50
+ * forward the caller's initialize instead of letting Client.connect synthesize
51
+ * an earlier initialize without its metadata. The server owns view selection
52
+ * and request overrides; the proxy never supplies a tools/list selector.
53
+ *
54
+ * The handshake fails closed: traffic before initialize, an initialize the
55
+ * server's selector contract would refuse, a second initialize, or a handshake
56
+ * that does not complete within the bound gets a JSON-RPC error (when it has
57
+ * an id) and closes both transports. Nothing invalid reaches the server.
44
58
  */
59
+ export declare function connectProxyTransport(options: ProxyOptions, downstream: Transport, bridge?: ProxyBridgeOptions): Promise<Transport>;
60
+ /** Run the published stdio executable with the caller's initialize intact. */
45
61
  export declare function runProxy(env?: NodeJS.ProcessEnv): Promise<void>;
46
62
  //# sourceMappingURL=proxy.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"proxy.d.ts","sourceRoot":"","sources":["../../src/proxy.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AAEnE,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AA0BnE;;;;;;;;;;;;;;;GAeG;AAEH,MAAM,WAAW,YAAY;IAC3B,sFAAsF;IACtF,SAAS,EAAE,MAAM,CAAC;IAClB,wDAAwD;IACxD,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC/B,4EAA4E;IAC5E,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC7B;AAsBD,wBAAgB,mBAAmB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,YAAY,CAqBtF;AAED,mFAAmF;AACnF,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,YAAY,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAUlF;AAED,oFAAoF;AACpF,wBAAsB,eAAe,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC,CAS5E;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CA+C1D;AAOD;;;;GAIG;AACH,wBAAsB,QAAQ,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,OAAO,CAAC,IAAI,CAAC,CAgBlF"}
1
+ {"version":3,"file":"proxy.d.ts","sourceRoot":"","sources":["../../src/proxy.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AAGnE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,+CAA+C,CAAC;AAC/E,OAAO,EAEL,KAAK,iBAAiB,EAQvB,MAAM,oCAAoC,CAAC;AAK5C;;;;;;;;;;;;;;;;;GAiBG;AAEH,MAAM,WAAW,YAAY;IAC3B,sFAAsF;IACtF,SAAS,EAAE,MAAM,CAAC;IAClB,wDAAwD;IACxD,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC/B,4EAA4E;IAC5E,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC7B;AAsBD,wBAAgB,mBAAmB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,YAAY,CAqBtF;AAED,mFAAmF;AACnF,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,YAAY,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAUlF;AASD;;;;GAIG;AACH,wBAAsB,eAAe,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC,CAI5E;AAED,4EAA4E;AAC5E,eAAO,MAAM,sBAAsB,2BAA2B,CAAC;AAI/D,uEAAuE;AACvE,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,iBAAiB,GAAG,MAAM,GAAG,SAAS,CAO/E;AAgBD,MAAM,WAAW,kBAAkB;IACjC,gFAAgF;IAChF,mBAAmB,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,qBAAqB,CACzC,OAAO,EAAE,YAAY,EACrB,UAAU,EAAE,SAAS,EACrB,MAAM,GAAE,kBAAuB,GAC9B,OAAO,CAAC,SAAS,CAAC,CA+FpB;AAED,8EAA8E;AAC9E,wBAAsB,QAAQ,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,OAAO,CAAC,IAAI,CAAC,CAelF"}
package/dist/src/proxy.js CHANGED
@@ -1,18 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
3
3
  import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
4
- import { Server } from "@modelcontextprotocol/sdk/server/index.js";
5
4
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
- import { CallToolRequestSchema, GetPromptRequestSchema, ListPromptsRequestSchema, ListResourceTemplatesRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, PromptListChangedNotificationSchema, ReadResourceRequestSchema, ResourceListChangedNotificationSchema, ToolListChangedNotificationSchema, } from "@modelcontextprotocol/sdk/types.js";
5
+ import { ErrorCode, InitializeRequestSchema, isInitializedNotification, isJSONRPCErrorResponse, isJSONRPCRequest, isJSONRPCResultResponse, } from "@modelcontextprotocol/sdk/types.js";
7
6
  import { requireSecureCredentialEndpoint } from "./credential-endpoint.js";
8
7
  import { isMainEntrypoint } from "./entrypoint.js";
9
8
  import { SERVER_VERSION } from "./index.js";
10
- // MCP permits server extensions such as Honua's tools/list `view`. The SDK's
11
- // nested params schema strips unknown keys by default, so preserve them before
12
- // forwarding or a stdio caller silently receives the default catalog instead.
13
- const ForwardedListToolsRequestSchema = ListToolsRequestSchema.extend({
14
- params: ListToolsRequestSchema.shape.params.unwrap().passthrough().optional(),
15
- });
16
9
  function isConfigured(value) {
17
10
  return value !== undefined && value.length > 0;
18
11
  }
@@ -58,84 +51,172 @@ export function buildUpstreamHeaders(options) {
58
51
  }
59
52
  return headers;
60
53
  }
61
- /** Connect an upstream MCP client to the remote honua /mcp over streamable HTTP. */
62
- export async function connectUpstream(options) {
54
+ function createUpstreamTransport(options) {
63
55
  const headers = buildUpstreamHeaders(options);
64
- const remoteUrl = validateProxyOptions(options);
65
- const transport = new StreamableHTTPClientTransport(remoteUrl, {
56
+ return new StreamableHTTPClientTransport(validateProxyOptions(options), {
66
57
  requestInit: { ...(Object.keys(headers).length > 0 ? { headers } : {}), redirect: "manual" },
67
58
  });
59
+ }
60
+ /**
61
+ * Connect a direct MCP client to the remote honua /mcp over streamable HTTP.
62
+ * Certification and evals use it as the HTTP reference side; the stdio proxy
63
+ * does not, because Client.connect would send its own initialize.
64
+ */
65
+ export async function connectUpstream(options) {
68
66
  const client = new Client({ name: "honua-mcp-stdio-proxy", version: SERVER_VERSION });
69
- await client.connect(transport);
67
+ await client.connect(createUpstreamTransport(options));
70
68
  return client;
71
69
  }
70
+ /** The server's only initialize metadata key; the session view selector. */
71
+ export const WORKFLOW_VIEW_META_KEY = "honua.io/workflow-view";
72
+ const MAX_WORKFLOW_VIEW_LENGTH = 64;
73
+ const INITIALIZE_TIMEOUT_MS = 30_000;
74
+ /** Read the optional view selector under the server's own contract. */
75
+ export function readWorkflowView(request) {
76
+ const value = request.params._meta?.[WORKFLOW_VIEW_META_KEY];
77
+ if (value === undefined || value === null)
78
+ return undefined;
79
+ if (typeof value !== "string" || value.length > MAX_WORKFLOW_VIEW_LENGTH) {
80
+ throw new Error("Invalid workflow view selector");
81
+ }
82
+ return value.trim().length === 0 ? undefined : value;
83
+ }
72
84
  /**
73
- * Build a low-level MCP server that forwards every request and `list_changed`
74
- * notification to the already-connected upstream client. The proxy advertises
75
- * exactly the upstream's name, version, and capabilities, so the downstream
76
- * (stdio) surface mirrors the upstream (HTTP-SSE) surface.
85
+ * Validate the caller's initialize and narrow its `_meta` to the recognised
86
+ * view selector. Every other field is the caller's own and passes unchanged;
87
+ * other metadata stays local rather than entering the credentialed session.
77
88
  */
78
- export function createProxyServer(upstream) {
79
- const upstreamInfo = upstream.getServerVersion() ?? { name: "honua", version: SERVER_VERSION };
80
- const upstreamCapabilities = upstream.getServerCapabilities() ?? {};
81
- const server = new Server({ name: upstreamInfo.name, version: upstreamInfo.version }, {
82
- capabilities: upstreamCapabilities,
83
- instructions: upstream.getInstructions(),
84
- });
85
- // ── Tools ──────────────────────────────────────────────────────
86
- if (upstreamCapabilities.tools) {
87
- server.setRequestHandler(ForwardedListToolsRequestSchema, async (request) => upstream.listTools(request.params));
88
- server.setRequestHandler(CallToolRequestSchema, async (request) => upstream.callTool(request.params));
89
- }
90
- // ── Resources ──────────────────────────────────────────────────
91
- if (upstreamCapabilities.resources) {
92
- server.setRequestHandler(ListResourcesRequestSchema, async (request) => upstream.listResources(request.params));
93
- server.setRequestHandler(ListResourceTemplatesRequestSchema, async (request) => upstream.listResourceTemplates(request.params));
94
- server.setRequestHandler(ReadResourceRequestSchema, async (request) => upstream.readResource(request.params));
95
- }
96
- // ── Prompts ────────────────────────────────────────────────────
97
- if (upstreamCapabilities.prompts) {
98
- server.setRequestHandler(ListPromptsRequestSchema, async (request) => upstream.listPrompts(request.params));
99
- server.setRequestHandler(GetPromptRequestSchema, async (request) => upstream.getPrompt(request.params));
100
- }
101
- // ── list_changed notification forwarding ───────────────────────
102
- // Keeps the stdio surface live: when the server's catalog changes, the
103
- // downstream client is told, exactly as an HTTP-SSE client would be.
104
- upstream.setNotificationHandler(ToolListChangedNotificationSchema, async () => {
105
- await server.sendToolListChanged();
106
- });
107
- upstream.setNotificationHandler(ResourceListChangedNotificationSchema, async () => {
108
- await server.sendResourceListChanged();
109
- });
110
- upstream.setNotificationHandler(PromptListChangedNotificationSchema, async () => {
111
- await server.sendPromptListChanged();
112
- });
113
- return server;
89
+ function upstreamInitialize(message) {
90
+ const view = readWorkflowView(InitializeRequestSchema.parse(message));
91
+ const { _meta: _ignored, ...params } = message.params ?? {};
92
+ return {
93
+ ...message,
94
+ params: { ...params, ...(view === undefined ? {} : { _meta: { [WORKFLOW_VIEW_META_KEY]: view } }) },
95
+ };
114
96
  }
115
- /* v8 ignore start -- live-process entry: wires the stdio transport to a real
116
- remote /mcp upstream; exercised by running the proxy, not by unit tests. The
117
- unit-testable logic (option/header resolution, upstream connect, catalog
118
- forwarding) lives in the exported functions above and is covered there. */
119
97
  /**
120
- * Run the stdio proxy end-to-end: resolve the remote `/mcp` from the
121
- * environment, connect upstream, and expose the mirrored catalog over stdio.
122
- * Also used by the deprecated `honua-mcp` bin, which now delegates here.
98
+ * Bridge one downstream transport to one upstream HTTP session. In particular,
99
+ * forward the caller's initialize instead of letting Client.connect synthesize
100
+ * an earlier initialize without its metadata. The server owns view selection
101
+ * and request overrides; the proxy never supplies a tools/list selector.
102
+ *
103
+ * The handshake fails closed: traffic before initialize, an initialize the
104
+ * server's selector contract would refuse, a second initialize, or a handshake
105
+ * that does not complete within the bound gets a JSON-RPC error (when it has
106
+ * an id) and closes both transports. Nothing invalid reaches the server.
123
107
  */
124
- export async function runProxy(env = process.env) {
125
- const options = resolveProxyOptions(env);
126
- const upstream = await connectUpstream(options);
127
- const server = createProxyServer(upstream);
128
- // Tear down both ends together so a dropped upstream surfaces to the client.
108
+ export async function connectProxyTransport(options, downstream, bridge = {}) {
109
+ const upstream = createUpstreamTransport(options);
110
+ let closed = false;
111
+ let refused = false;
112
+ let initializeSeen = false;
113
+ let initializeId;
114
+ let initialized = Promise.resolve();
129
115
  const shutdown = async () => {
130
- await server.close().catch(() => { });
131
- await upstream.close().catch(() => { });
116
+ if (closed)
117
+ return;
118
+ closed = true;
119
+ clearTimeout(handshake);
120
+ await Promise.all([downstream.close().catch(() => { }), upstream.close().catch(() => { })]);
121
+ };
122
+ // Reply before closing so the caller learns why its session ended. The
123
+ // message is the proxy's own; nothing from upstream is echoed.
124
+ const refuse = (id, code, reason) => {
125
+ if (closed || refused)
126
+ return;
127
+ refused = true;
128
+ const reply = id === undefined ? Promise.resolve() : downstream.send({ jsonrpc: "2.0", id, error: { code, message: reason } });
129
+ void reply.catch(() => { }).finally(() => shutdown());
130
+ };
131
+ const handshake = setTimeout(() => refuse(initializeId, ErrorCode.RequestTimeout, "Proxy initialization timed out"), bridge.initializeTimeoutMs ?? INITIALIZE_TIMEOUT_MS);
132
+ downstream.onclose = upstream.onclose = () => {
133
+ void shutdown();
132
134
  };
133
- upstream.onclose = () => {
135
+ // HTTP/SSE errors can contain credential-bearing response bodies. Close
136
+ // both transports on synchronous or background failures without echoing
137
+ // those details. In particular, a failed SSE reader must not strand stdio.
138
+ upstream.onerror = () => {
134
139
  void shutdown();
135
140
  };
141
+ downstream.onmessage = (message) => {
142
+ if (closed || refused)
143
+ return;
144
+ let forwarded = message;
145
+ const isInitialize = isJSONRPCRequest(message) && message.method === "initialize";
146
+ if (isJSONRPCRequest(message) && message.method === "initialize") {
147
+ if (initializeSeen) {
148
+ refuse(message.id, ErrorCode.InvalidRequest, "Duplicate initialize is not permitted");
149
+ return;
150
+ }
151
+ try {
152
+ forwarded = upstreamInitialize(message);
153
+ }
154
+ catch {
155
+ refuse(message.id, ErrorCode.InvalidRequest, "Invalid initialize request or workflow view selector");
156
+ return;
157
+ }
158
+ initializeSeen = true;
159
+ initializeId = message.id;
160
+ }
161
+ else if (!initializeSeen) {
162
+ refuse(isJSONRPCRequest(message) ? message.id : undefined, ErrorCode.InvalidRequest, "Initialize must be the first request");
163
+ return;
164
+ }
165
+ const sending = initialized.then(() => upstream.send(forwarded));
166
+ // HTTP POSTs may otherwise overtake initialize (which establishes the
167
+ // session) or the initialized notification. Await their acceptance before
168
+ // subsequent traffic, while allowing later tool calls and cancellation
169
+ // notifications to proceed concurrently.
170
+ if (isInitialize || isInitializedNotification(message))
171
+ initialized = sending;
172
+ void sending.catch(() => {
173
+ void shutdown();
174
+ });
175
+ };
176
+ upstream.onmessage = (message) => {
177
+ if (initializeId !== undefined &&
178
+ (isJSONRPCResultResponse(message) || isJSONRPCErrorResponse(message)) &&
179
+ message.id === initializeId) {
180
+ clearTimeout(handshake);
181
+ initializeId = undefined;
182
+ if (isJSONRPCResultResponse(message) && typeof message.result.protocolVersion === "string") {
183
+ upstream.setProtocolVersion(message.result.protocolVersion);
184
+ downstream.setProtocolVersion?.(message.result.protocolVersion);
185
+ }
186
+ }
187
+ void downstream.send(message).catch(() => {
188
+ void shutdown();
189
+ });
190
+ };
191
+ try {
192
+ await upstream.start();
193
+ await downstream.start();
194
+ }
195
+ catch (error) {
196
+ await shutdown();
197
+ throw error;
198
+ }
199
+ return upstream;
200
+ }
201
+ /** Run the published stdio executable with the caller's initialize intact. */
202
+ export async function runProxy(env = process.env) {
203
+ const options = resolveProxyOptions(env);
136
204
  const transport = new StdioServerTransport();
137
- await server.connect(transport);
205
+ const close = () => {
206
+ void transport.close().catch(() => { });
207
+ };
208
+ process.stdin.once("end", close);
209
+ process.stdin.once("close", close);
210
+ try {
211
+ await connectProxyTransport(options, transport);
212
+ }
213
+ catch (error) {
214
+ process.stdin.off("end", close);
215
+ process.stdin.off("close", close);
216
+ throw error;
217
+ }
138
218
  }
219
+ /* v8 ignore start -- process entry; the transport bridge is tested above. */
139
220
  if (isMainEntrypoint(import.meta.url)) {
140
221
  runProxy().catch((err) => {
141
222
  process.stderr.write(`Fatal: ${err instanceof Error ? err.message : String(err)}\n`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@honua/mcp-server",
3
- "version": "0.1.11-beta.0",
3
+ "version": "0.1.13",
4
4
  "description": "Platform-free MCP server for geospatial services: expose discovery, query, and analysis over any public ArcGIS/Esri FeatureServer or OGC API endpoint to any MCP client, with no platform lock-in",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Honua",
@@ -85,11 +85,11 @@
85
85
  "overrides": {
86
86
  "hono": "^4.13.7",
87
87
  "@hono/node-server": "^2.0.5",
88
- "fast-uri": "^3.1.6",
88
+ "fast-uri": "^3.1.8",
89
89
  "qs": "^6.16.0"
90
90
  },
91
91
  "peerDependencies": {
92
- "@honua/sdk-js": "^0.1.11-beta.0"
92
+ "@honua/sdk-js": "^0.1.13"
93
93
  },
94
94
  "devDependencies": {
95
95
  "@biomejs/biome": "1.9.4",
@@ -2,7 +2,7 @@
2
2
  "mcpServers": {
3
3
  "honua": {
4
4
  "command": "npx",
5
- "args": ["-y", "--package", "@honua/mcp-server@0.1.10-beta.0", "honua-mcp-proxy"],
5
+ "args": ["-y", "--package", "@honua/mcp-server@0.1.12", "honua-mcp-proxy"],
6
6
  "env": {
7
7
  "HONUA_MCP_REMOTE_URL": "http://localhost:8080/mcp"
8
8
  }
@@ -2,7 +2,7 @@
2
2
  "mcpServers": {
3
3
  "honua": {
4
4
  "command": "npx",
5
- "args": ["-y", "--package", "@honua/mcp-server@0.1.10-beta.0", "honua-mcp-proxy"],
5
+ "args": ["-y", "--package", "@honua/mcp-server@0.1.12", "honua-mcp-proxy"],
6
6
  "env": {
7
7
  "HONUA_MCP_REMOTE_URL": "http://localhost:8080/mcp"
8
8
  }
@@ -2,7 +2,7 @@
2
2
  "mcpServers": {
3
3
  "honua": {
4
4
  "command": "npx",
5
- "args": ["-y", "--package", "@honua/mcp-server@0.1.10-beta.0", "honua-mcp-proxy"],
5
+ "args": ["-y", "--package", "@honua/mcp-server@0.1.12", "honua-mcp-proxy"],
6
6
  "env": {
7
7
  "HONUA_MCP_REMOTE_URL": "http://localhost:8080/mcp"
8
8
  }