@emseepea/create-soap-backed-server 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (25) hide show
  1. package/README.md +96 -0
  2. package/initializer-dist/LICENSE +21 -0
  3. package/initializer-dist/create.mjs +44 -0
  4. package/initializer-dist/template/README.md +68 -0
  5. package/initializer-dist/template/contracts/pea-service.wsdl +30 -0
  6. package/initializer-dist/template/contracts/pea-service.xsd +29 -0
  7. package/initializer-dist/template/contracts/soap-envelope.xsd +20 -0
  8. package/initializer-dist/template/eval/meaning.test.mjs +25 -0
  9. package/initializer-dist/template/package.json +35 -0
  10. package/initializer-dist/template/scripts/check-generated.mjs +15 -0
  11. package/initializer-dist/template/scripts/generate-types.mjs +42 -0
  12. package/initializer-dist/template/src/app.ts +26 -0
  13. package/initializer-dist/template/src/capabilities/context.ts +5 -0
  14. package/initializer-dist/template/src/capabilities/tool.get-pea-variety.ts +41 -0
  15. package/initializer-dist/template/src/generated/pea-service.schema-model.json +205 -0
  16. package/initializer-dist/template/src/generated/pea-service.ts +34 -0
  17. package/initializer-dist/template/src/server.ts +21 -0
  18. package/initializer-dist/template/src/soap-schema.ts +28 -0
  19. package/initializer-dist/template/src/validating-http-client.ts +125 -0
  20. package/initializer-dist/template/test/server.test.mjs +250 -0
  21. package/initializer-dist/template/test-support/soap-fixture.mjs +160 -0
  22. package/initializer-dist/template/test-types/generated-soap-types.ts +45 -0
  23. package/initializer-dist/template/tsconfig.json +13 -0
  24. package/initializer-dist/template/tsconfig.type-tests.json +8 -0
  25. package/package.json +59 -0
package/README.md ADDED
@@ -0,0 +1,96 @@
1
+ # `@emseepea/create-soap-backed-server`
2
+
3
+ This directory is both the maintained example and the template source for its
4
+ public npm initializer.
5
+
6
+ ## Use This Template
7
+
8
+ Use this template when an MCP tool must call an existing SOAP service whose
9
+ WSDL and XSD remain authoritative. It shows local contract loading, generated
10
+ TypeScript, raw XML validation before parsing, and a small public MCP schema.
11
+
12
+ Choose the [API-backed server](../api-backed-server/README.md) for JSON over
13
+ HTTP, or the [tool server](../tool-server/README.md) when no backend service is
14
+ needed. [Compare all templates](https://emseepea.github.io/emseepea/examples/).
15
+
16
+ ## Create a Project
17
+
18
+ ```sh
19
+ npm init @emseepea/soap-backed-server -- my-server
20
+ ```
21
+
22
+ The command creates a standalone app in a new `my-server` directory and sets
23
+ `private: true` in its `package.json` so the app cannot be published by
24
+ accident. It includes local service contracts, checked-in generated types,
25
+ lint checks, ordinary tests, and a semantic test.
26
+
27
+ <!-- generated-project-readme -->
28
+
29
+ ## SOAP-Backed Server
30
+
31
+ The committed WSDL defines the operation and imports the committed service XSD.
32
+ That XSD generates `src/generated/pea-service.ts`. The same XSD is imported by
33
+ the SOAP-envelope schema used to validate every raw response before the `soap`
34
+ library parses it.
35
+
36
+ Normal builds and starts use checked-in generated types and local contracts.
37
+ They never fetch a WSDL or XSD. Compatible backend values pass through without
38
+ translation tables.
39
+
40
+ ## Configure the Service
41
+
42
+ Set the one fixed SOAP endpoint, then build and start:
43
+
44
+ ```sh
45
+ npm install
46
+ npm run build
47
+ PEA_SOAP_URL=https://soap.example.test/pea npm start
48
+ ```
49
+
50
+ Keep service credentials outside the URL and source code. Add authentication
51
+ headers inside the server for your provider. Never accept an endpoint, schema
52
+ location, SOAP action, or arbitrary XML from an MCP caller.
53
+
54
+ ## Change the Contract
55
+
56
+ Edit the local files under `contracts/`, then regenerate and review the diff:
57
+
58
+ ```sh
59
+ npm run generate
60
+ npm run generate:check
61
+ ```
62
+
63
+ The generator maps the XSD's required fields, optional fields, arrays, numeric
64
+ types, and named types into TypeScript. The runtime XSD validator enforces the
65
+ actual restrictions.
66
+
67
+ ## Safety Boundary
68
+
69
+ The transport accepts responses only from `PEA_SOAP_URL`. It does not follow
70
+ redirects. It rejects requests or responses over 64 KiB, DTDs, entity
71
+ declarations, malformed XML, XSD-invalid XML, SOAP faults, and responses taking
72
+ longer than 1.5 seconds. Schema imports are restricted to the committed local
73
+ service XSD. Public failures contain no provider details.
74
+
75
+ ## Tool
76
+
77
+ `get-pea-variety` retrieves one variety and returns described JSON. The MCP
78
+ caller never sees SOAP XML or chooses transport details.
79
+
80
+ ## Check This Project
81
+
82
+ Run generation, lint, build, and ordinary tests without spending model tokens:
83
+
84
+ ```sh
85
+ npm run generate:check
86
+ npm run lint
87
+ npm test
88
+ ```
89
+
90
+ Run the more expensive AI test separately:
91
+
92
+ ```sh
93
+ npm run test:llm
94
+ ```
95
+
96
+ If Claude is not already signed in, run `claude auth login` first.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Windy Road Technology Pty. Limited
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,44 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { cp, mkdir, mkdtemp, readFile, readdir, rm, writeFile } from "node:fs/promises";
4
+ import { basename, dirname, join, resolve } from "node:path";
5
+
6
+ const [destination, ...extra] = process.argv.slice(2);
7
+ if (extra.length > 0 || !destination || !/^[a-z0-9][a-z0-9._-]*$/.test(destination)) {
8
+ throw new Error("Provide one simple lowercase destination name, such as my-server");
9
+ }
10
+
11
+ const target = resolve(destination);
12
+ if (basename(target) !== destination) throw new Error("The destination must not contain a path");
13
+ const staging = await mkdtemp(join(dirname(target), ".emseepea-create-"));
14
+
15
+ try {
16
+ await copyContents(new URL("./template/", import.meta.url), staging);
17
+ const manifestPath = resolve(staging, "package.json");
18
+ const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
19
+ await writeFile(manifestPath, `${JSON.stringify({ ...manifest, name: destination }, null, 2)}\n`);
20
+ await mkdir(target);
21
+ try {
22
+ await copyContents(staging, target);
23
+ } catch (error) {
24
+ await rm(target, { recursive: true, force: true });
25
+ throw error;
26
+ }
27
+ } catch (error) {
28
+ if (["EEXIST", "ENOTEMPTY"].includes(error.code)) {
29
+ throw new Error(`The destination already exists: ${destination}`);
30
+ }
31
+ throw error;
32
+ } finally {
33
+ await rm(staging, { recursive: true, force: true });
34
+ }
35
+
36
+ async function copyContents(source, destination) {
37
+ for (const entry of await readdir(source)) {
38
+ const from = source instanceof URL ? new URL(entry, source) : join(source, entry);
39
+ await cp(from, join(destination, entry), { recursive: true, errorOnExist: true, force: false });
40
+ }
41
+ }
42
+
43
+ console.log(`Created ${destination}.`);
44
+ console.log(`Next: cd ${destination}; npm install; npm test; npm start`);
@@ -0,0 +1,68 @@
1
+ # SOAP-Backed Server
2
+
3
+ The committed WSDL defines the operation and imports the committed service XSD.
4
+ That XSD generates `src/generated/pea-service.ts`. The same XSD is imported by
5
+ the SOAP-envelope schema used to validate every raw response before the `soap`
6
+ library parses it.
7
+
8
+ Normal builds and starts use checked-in generated types and local contracts.
9
+ They never fetch a WSDL or XSD. Compatible backend values pass through without
10
+ translation tables.
11
+
12
+ ## Configure the Service
13
+
14
+ Set the one fixed SOAP endpoint, then build and start:
15
+
16
+ ```sh
17
+ npm install
18
+ npm run build
19
+ PEA_SOAP_URL=https://soap.example.test/pea npm start
20
+ ```
21
+
22
+ Keep service credentials outside the URL and source code. Add authentication
23
+ headers inside the server for your provider. Never accept an endpoint, schema
24
+ location, SOAP action, or arbitrary XML from an MCP caller.
25
+
26
+ ## Change the Contract
27
+
28
+ Edit the local files under `contracts/`, then regenerate and review the diff:
29
+
30
+ ```sh
31
+ npm run generate
32
+ npm run generate:check
33
+ ```
34
+
35
+ The generator maps the XSD's required fields, optional fields, arrays, numeric
36
+ types, and named types into TypeScript. The runtime XSD validator enforces the
37
+ actual restrictions.
38
+
39
+ ## Safety Boundary
40
+
41
+ The transport accepts responses only from `PEA_SOAP_URL`. It does not follow
42
+ redirects. It rejects requests or responses over 64 KiB, DTDs, entity
43
+ declarations, malformed XML, XSD-invalid XML, SOAP faults, and responses taking
44
+ longer than 1.5 seconds. Schema imports are restricted to the committed local
45
+ service XSD. Public failures contain no provider details.
46
+
47
+ ## Tool
48
+
49
+ `get-pea-variety` retrieves one variety and returns described JSON. The MCP
50
+ caller never sees SOAP XML or chooses transport details.
51
+
52
+ ## Check This Project
53
+
54
+ Run generation, lint, build, and ordinary tests without spending model tokens:
55
+
56
+ ```sh
57
+ npm run generate:check
58
+ npm run lint
59
+ npm test
60
+ ```
61
+
62
+ Run the more expensive AI test separately:
63
+
64
+ ```sh
65
+ npm run test:llm
66
+ ```
67
+
68
+ If Claude is not already signed in, run `claude auth login` first.
@@ -0,0 +1,30 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <definitions xmlns="http://schemas.xmlsoap.org/wsdl/"
3
+ xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
4
+ xmlns:xs="http://www.w3.org/2001/XMLSchema"
5
+ xmlns:pea="urn:emseepea:pea-service"
6
+ targetNamespace="urn:emseepea:pea-service">
7
+ <types>
8
+ <xs:schema>
9
+ <xs:import namespace="urn:emseepea:pea-service" schemaLocation="pea-service.xsd"/>
10
+ </xs:schema>
11
+ </types>
12
+ <message name="GetPeaInput"><part name="parameters" element="pea:GetPeaRequest"/></message>
13
+ <message name="GetPeaOutput"><part name="parameters" element="pea:GetPeaResponse"/></message>
14
+ <portType name="PeaServicePortType">
15
+ <operation name="GetPea"><input message="pea:GetPeaInput"/><output message="pea:GetPeaOutput"/></operation>
16
+ </portType>
17
+ <binding name="PeaServiceBinding" type="pea:PeaServicePortType">
18
+ <soap:binding transport="http://schemas.xmlsoap.org/soap/http" style="document"/>
19
+ <operation name="GetPea">
20
+ <soap:operation soapAction="GetPea"/>
21
+ <input><soap:body use="literal"/></input>
22
+ <output><soap:body use="literal"/></output>
23
+ </operation>
24
+ </binding>
25
+ <service name="PeaService">
26
+ <port name="PeaServicePort" binding="pea:PeaServiceBinding">
27
+ <soap:address location="http://127.0.0.1:3999/soap"/>
28
+ </port>
29
+ </service>
30
+ </definitions>
@@ -0,0 +1,29 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
3
+ xmlns:pea="urn:emseepea:pea-service"
4
+ targetNamespace="urn:emseepea:pea-service"
5
+ elementFormDefault="qualified">
6
+ <xs:simpleType name="PeaType">
7
+ <xs:restriction base="xs:string">
8
+ <xs:minLength value="1"/>
9
+ <xs:maxLength value="40"/>
10
+ </xs:restriction>
11
+ </xs:simpleType>
12
+ <xs:complexType name="PeaVariety">
13
+ <xs:sequence>
14
+ <xs:element name="name" type="xs:string"/>
15
+ <xs:element name="peaType" type="pea:PeaType"/>
16
+ <xs:element name="daysToMaturity" type="xs:positiveInteger"/>
17
+ <xs:element name="note" type="xs:string" minOccurs="0"/>
18
+ <xs:element name="trait" type="xs:string" minOccurs="0" maxOccurs="5"/>
19
+ </xs:sequence>
20
+ </xs:complexType>
21
+ <xs:element name="GetPeaRequest">
22
+ <xs:complexType>
23
+ <xs:sequence>
24
+ <xs:element name="name" type="xs:string"/>
25
+ </xs:sequence>
26
+ </xs:complexType>
27
+ </xs:element>
28
+ <xs:element name="GetPeaResponse" type="pea:PeaVariety"/>
29
+ </xs:schema>
@@ -0,0 +1,20 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
3
+ xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
4
+ xmlns:pea="urn:emseepea:pea-service"
5
+ targetNamespace="http://schemas.xmlsoap.org/soap/envelope/"
6
+ elementFormDefault="qualified">
7
+ <xs:import namespace="urn:emseepea:pea-service" schemaLocation="pea-service.xsd"/>
8
+ <xs:complexType name="SoapBody">
9
+ <xs:choice>
10
+ <xs:element ref="pea:GetPeaResponse"/>
11
+ <xs:element name="Fault" type="xs:anyType"/>
12
+ </xs:choice>
13
+ </xs:complexType>
14
+ <xs:complexType name="SoapEnvelope">
15
+ <xs:sequence>
16
+ <xs:element name="Body" type="soap:SoapBody"/>
17
+ </xs:sequence>
18
+ </xs:complexType>
19
+ <xs:element name="Envelope" type="soap:SoapEnvelope"/>
20
+ </xs:schema>
@@ -0,0 +1,25 @@
1
+ import test from "node:test";
2
+ import {
3
+ assertResponseMeaning,
4
+ assertToolCalls,
5
+ createConversation,
6
+ } from "@emseepea/testing/semantic";
7
+ import { startSoapFixture } from "../test-support/soap-fixture.mjs";
8
+
9
+ test("retrieves a pea variety from SOAP through a natural request", async (t) => {
10
+ const fixture = await startSoapFixture(t);
11
+ const chat = await createConversation(t, {
12
+ server: new URL("../dist/server.js", import.meta.url),
13
+ environment: { PEA_SOAP_URL: fixture.url.href },
14
+ });
15
+
16
+ // One turn covers the example's only model-visible decision. XML validation
17
+ // belongs in deterministic tests and would only waste model calls here.
18
+ const result = await chat.send("Tell me about the Sugar Ann pea variety.");
19
+ assertToolCalls(result, [{ name: "get-pea-variety", arguments: { name: "Sugar Ann" } }]);
20
+ await assertResponseMeaning(result, {
21
+ expected:
22
+ "Sugar Ann is a snap pea that typically matures in 56 days. " +
23
+ "It is an early bush variety with compact plants and edible pods.",
24
+ });
25
+ });
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "emseepea-starter",
3
+ "version": "0.0.0",
4
+ "description": "Create an Em See Pea server that validates a SOAP service from its XSD contract.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "scripts": {
8
+ "build": "tsc -p tsconfig.json && tsc -p tsconfig.type-tests.json",
9
+ "generate": "node scripts/generate-types.mjs",
10
+ "generate:check": "node scripts/check-generated.mjs",
11
+ "start": "node dist/server.js",
12
+ "test": "npm run build && npm run test:built",
13
+ "test:built": "node --test test/*.test.mjs",
14
+ "test:llm": "npm run build && npm run test:llm:built",
15
+ "test:llm:built": "emseepea-test eval",
16
+ "lint": "oxlint src test test-types eval scripts test-support"
17
+ },
18
+ "devDependencies": {
19
+ "@emseepea/testing": "0.5.2",
20
+ "@types/node": "24.13.3",
21
+ "typescript": "6.0.3",
22
+ "oxlint": "1.80.0"
23
+ },
24
+ "engines": {
25
+ "node": ">=22.13.0"
26
+ },
27
+ "private": true,
28
+ "dependencies": {
29
+ "@emseepea/server": "0.3.3",
30
+ "@types/sax": "1.2.7",
31
+ "soap": "1.11.0",
32
+ "xml-xsd-engine": "1.7.3",
33
+ "zod": "4.4.3"
34
+ }
35
+ }
@@ -0,0 +1,15 @@
1
+ import assert from "node:assert/strict";
2
+ import { readFile } from "node:fs/promises";
3
+ import { generatedArtifacts } from "./generate-types.mjs";
4
+
5
+ const generated = await generatedArtifacts();
6
+ assert.equal(
7
+ await readFile(new URL("../src/generated/pea-service.ts", import.meta.url), "utf8"),
8
+ generated.types,
9
+ "generated SOAP types are stale",
10
+ );
11
+ assert.equal(
12
+ await readFile(new URL("../src/generated/pea-service.schema-model.json", import.meta.url), "utf8"),
13
+ generated.model,
14
+ "generated SOAP schema model is stale",
15
+ );
@@ -0,0 +1,42 @@
1
+ import { readFile, writeFile } from "node:fs/promises";
2
+ import { fileURLToPath } from "node:url";
3
+ import { generateTypeScript, parseXsdAsync } from "xml-xsd-engine";
4
+
5
+ const envelope = new URL("../contracts/soap-envelope.xsd", import.meta.url);
6
+ const service = new URL("../contracts/pea-service.xsd", import.meta.url);
7
+
8
+ export async function generatedArtifacts() {
9
+ const serviceSource = await readFile(service, "utf8");
10
+ return artifactsFromSources(await readFile(envelope, "utf8"), serviceSource);
11
+ }
12
+
13
+ export async function artifactsFromSources(envelopeSource, serviceSource) {
14
+ const schema = await parseXsdAsync(envelopeSource, async (location, namespace) => {
15
+ if (location !== "pea-service.xsd" || namespace !== "urn:emseepea:pea-service") {
16
+ throw new Error("Blocked schema import");
17
+ }
18
+ return serviceSource;
19
+ });
20
+ const types = generateTypeScript(schema, {
21
+ exportAll: true,
22
+ header: "// Generated from the local SOAP XSD graph. Do not edit.",
23
+ typeMap: {
24
+ "{http://schemas.xmlsoap.org/soap/envelope/}SoapBody": "SoapBody",
25
+ "{http://schemas.xmlsoap.org/soap/envelope/}SoapEnvelope": "SoapEnvelope",
26
+ "{urn:emseepea:pea-service}PeaType": "PeaType",
27
+ "{urn:emseepea:pea-service}PeaVariety": "PeaVariety",
28
+ },
29
+ });
30
+ return {
31
+ model: `${JSON.stringify(schema.toJSON(), null, 2)}\n`,
32
+ types: `${types.trim()}\n`,
33
+ };
34
+ }
35
+
36
+ if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
37
+ const generated = await generatedArtifacts();
38
+ await Promise.all([
39
+ writeFile(new URL("../src/generated/pea-service.ts", import.meta.url), generated.types),
40
+ writeFile(new URL("../src/generated/pea-service.schema-model.json", import.meta.url), generated.model),
41
+ ]);
42
+ }
@@ -0,0 +1,26 @@
1
+ import { fileURLToPath } from "node:url";
2
+ import { createEmseepea, discoverCapabilities } from "@emseepea/server";
3
+ import { createClientAsync } from "soap";
4
+ import { z } from "zod";
5
+ import { loadSoapEnvelopeSchema } from "./soap-schema.js";
6
+ import { ValidatingHttpClient } from "./validating-http-client.js";
7
+
8
+ export async function createSoapExample(endpointValue: string) {
9
+ const endpoint = new URL(z.string().url().parse(endpointValue));
10
+ if (!["http:", "https:"].includes(endpoint.protocol) || endpoint.username || endpoint.password || endpoint.hash) {
11
+ throw new Error("SOAP endpoint must be an HTTP address without credentials or a fragment");
12
+ }
13
+ const schema = await loadSoapEnvelopeSchema();
14
+ const client = await createClientAsync(
15
+ fileURLToPath(new URL("../contracts/pea-service.wsdl", import.meta.url)),
16
+ { disableCache: true, httpClient: new ValidatingHttpClient(endpoint, schema), strict: true },
17
+ endpoint.href,
18
+ );
19
+ const app = createEmseepea({
20
+ name: "emseepea-soap-backed-server",
21
+ version: "0.0.0",
22
+ instructions: "Retrieve pea variety details from a legacy SOAP service.",
23
+ ...await discoverCapabilities(new URL("./capabilities/", import.meta.url), { client }),
24
+ });
25
+ return { app };
26
+ }
@@ -0,0 +1,5 @@
1
+ import type { Client } from "soap";
2
+
3
+ export interface SoapExampleContext {
4
+ readonly client: Client;
5
+ }
@@ -0,0 +1,41 @@
1
+ import { defineTool, type CapabilityModuleFactory } from "@emseepea/server";
2
+ import { z } from "zod";
3
+ import type { GetPeaRequest, GetPeaResponse } from "../generated/pea-service.js";
4
+ import type { SoapExampleContext } from "./context.js";
5
+
6
+ const inputSchema = z.object({
7
+ name: z.string().trim().min(1).max(100).describe("Name of the pea variety to retrieve."),
8
+ });
9
+ const outputSchema = z.object({
10
+ name: z.string().min(1).max(100).describe("Name of the pea variety."),
11
+ peaType: z.string().min(1).max(40).describe("Type of pea."),
12
+ daysToMaturity: z.number().int().positive().describe("Typical number of days from sowing to harvest."),
13
+ note: z.string().max(500).optional().describe("Optional growing note."),
14
+ traits: z.array(z.string().max(100)).max(5).describe("Growing or eating traits."),
15
+ });
16
+
17
+ export default (({ client }) => defineTool({
18
+ name: "get-pea-variety",
19
+ access: "public",
20
+ description: "Get details about one pea variety.",
21
+ inputSchema,
22
+ outputSchema,
23
+ async handler({ name }, { signal }) {
24
+ signal.throwIfAborted();
25
+ const request: GetPeaRequest = { name };
26
+ const [response] = await client.GetPeaAsync(request, { signal });
27
+ signal.throwIfAborted();
28
+ return { data: outputSchema.parse(mapResponse(response)) };
29
+ },
30
+ })) satisfies CapabilityModuleFactory<SoapExampleContext>;
31
+
32
+ function mapResponse(response: GetPeaResponse): z.input<typeof outputSchema> {
33
+ return {
34
+ name: response.name,
35
+ peaType: response.peaType,
36
+ // node-soap returns validated XSD numeric text as a string.
37
+ daysToMaturity: Number(response.daysToMaturity),
38
+ note: response.note,
39
+ traits: response.trait ?? [],
40
+ };
41
+ }