@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.
- package/README.md +96 -0
- package/initializer-dist/LICENSE +21 -0
- package/initializer-dist/create.mjs +44 -0
- package/initializer-dist/template/README.md +68 -0
- package/initializer-dist/template/contracts/pea-service.wsdl +30 -0
- package/initializer-dist/template/contracts/pea-service.xsd +29 -0
- package/initializer-dist/template/contracts/soap-envelope.xsd +20 -0
- package/initializer-dist/template/eval/meaning.test.mjs +25 -0
- package/initializer-dist/template/package.json +35 -0
- package/initializer-dist/template/scripts/check-generated.mjs +15 -0
- package/initializer-dist/template/scripts/generate-types.mjs +42 -0
- package/initializer-dist/template/src/app.ts +26 -0
- package/initializer-dist/template/src/capabilities/context.ts +5 -0
- package/initializer-dist/template/src/capabilities/tool.get-pea-variety.ts +41 -0
- package/initializer-dist/template/src/generated/pea-service.schema-model.json +205 -0
- package/initializer-dist/template/src/generated/pea-service.ts +34 -0
- package/initializer-dist/template/src/server.ts +21 -0
- package/initializer-dist/template/src/soap-schema.ts +28 -0
- package/initializer-dist/template/src/validating-http-client.ts +125 -0
- package/initializer-dist/template/test/server.test.mjs +250 -0
- package/initializer-dist/template/test-support/soap-fixture.mjs +160 -0
- package/initializer-dist/template/test-types/generated-soap-types.ts +45 -0
- package/initializer-dist/template/tsconfig.json +13 -0
- package/initializer-dist/template/tsconfig.type-tests.json +8 -0
- 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,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
|
+
}
|