@emseepea/create-openapi-backed-server 0.0.0 → 0.0.1
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 +93 -0
- package/initializer-dist/LICENSE +21 -0
- package/initializer-dist/create.mjs +44 -0
- package/initializer-dist/template/README.md +74 -0
- package/initializer-dist/template/contracts/petstore.openapi.json +1 -0
- package/initializer-dist/template/eval/meaning.test.mjs +20 -0
- package/initializer-dist/template/package.json +38 -0
- package/initializer-dist/template/scripts/check-generated.mjs +22 -0
- package/initializer-dist/template/scripts/check-licenses.mjs +29 -0
- package/initializer-dist/template/scripts/generate.mjs +74 -0
- package/initializer-dist/template/src/app.ts +25 -0
- package/initializer-dist/template/src/capabilities/tool.get-pet.ts +60 -0
- package/initializer-dist/template/src/generated/petstore.ts +44 -0
- package/initializer-dist/template/src/generated/petstore.types.d.ts +62 -0
- package/initializer-dist/template/src/server.ts +22 -0
- package/initializer-dist/template/test/fixtures/petstore.swagger.yaml +38 -0
- package/initializer-dist/template/test/server.test.mjs +191 -0
- package/initializer-dist/template/test-support/llm-server.mjs +28 -0
- package/initializer-dist/template/test-support/petstore-fixture.mjs +8 -0
- package/initializer-dist/template/tsconfig.json +13 -0
- package/package.json +38 -8
package/README.md
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# `@emseepea/create-openapi-backed-server`
|
|
2
|
+
|
|
3
|
+
This directory is both the maintained example and its public npm initializer.
|
|
4
|
+
|
|
5
|
+
## Use This Template
|
|
6
|
+
|
|
7
|
+
Use this template when the JSON API you call has an OpenAPI 3 or Swagger 2
|
|
8
|
+
contract and you want generated TypeScript and Zod validation at the backend
|
|
9
|
+
boundary. Use the [API-backed server template](../api-backed-server/README.md)
|
|
10
|
+
when no usable machine-readable contract exists.
|
|
11
|
+
|
|
12
|
+
## Create a Project
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
npm init @emseepea/openapi-backed-server -- my-server
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
<!-- generated-project-readme -->
|
|
19
|
+
|
|
20
|
+
## OpenAPI-Backed Pet Lookup
|
|
21
|
+
|
|
22
|
+
The `get-pet` tool looks up one pet in
|
|
23
|
+
[Swagger Petstore](https://petstore3.swagger.io/) by a positive identifier.
|
|
24
|
+
The backend path parameters, response TypeScript declarations, and response
|
|
25
|
+
validator come from the committed OpenAPI contract. The public MCP input and
|
|
26
|
+
output remain separately described and bounded by the application.
|
|
27
|
+
|
|
28
|
+
The canonical contract is
|
|
29
|
+
[`contracts/petstore.openapi.json`](contracts/petstore.openapi.json). It was
|
|
30
|
+
retrieved on 11 September 2026 from the exact
|
|
31
|
+
[Swagger Petstore OpenAPI 3 endpoint](https://petstore3.swagger.io/api/v3/openapi.json)
|
|
32
|
+
at version `1.0.27`; the committed file has SHA-256
|
|
33
|
+
`f50a32e57d10018049bcd51c28bcb47f8e595563f7583cedaa9e7134376cec62`.
|
|
34
|
+
|
|
35
|
+
The corresponding
|
|
36
|
+
[upstream source at revision `d57941e`](https://github.com/swagger-api/swagger-petstore/blob/d57941e8fe959e508796b27469b1e8bba73392dc/src/main/resources/openapi.yaml)
|
|
37
|
+
identifies the contract as Apache-2.0 licensed.
|
|
38
|
+
|
|
39
|
+
The generation script accepts only local JSON Pointer references beginning
|
|
40
|
+
with `#/`. It rejects web and relative-file references before conversion and
|
|
41
|
+
again after Swagger 2 conversion. Generation, build, tests, start, and request
|
|
42
|
+
handling never fetch a contract.
|
|
43
|
+
|
|
44
|
+
## Generate Types and Validators
|
|
45
|
+
|
|
46
|
+
Edit the local contract, then regenerate and review the diff:
|
|
47
|
+
|
|
48
|
+
```sh
|
|
49
|
+
npm run generate
|
|
50
|
+
npm run generate:check
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The checked-in files under `src/generated/` use `typed-openapi` with Zod 4,
|
|
54
|
+
strict validation, the `getPetById` operation filter, and no generated HTTP
|
|
55
|
+
client. Normal builds use these files without running the generator.
|
|
56
|
+
|
|
57
|
+
The local
|
|
58
|
+
[`test/fixtures/petstore.swagger.yaml`](test/fixtures/petstore.swagger.yaml)
|
|
59
|
+
proves the older Swagger 2 import path. It is converted to OpenAPI 3 and sent
|
|
60
|
+
through the same generator; it is not a second application template.
|
|
61
|
+
|
|
62
|
+
## Run
|
|
63
|
+
|
|
64
|
+
```sh
|
|
65
|
+
npm install
|
|
66
|
+
npm run build
|
|
67
|
+
npm start
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
The MCP endpoint is `http://127.0.0.1:3000/mcp`.
|
|
71
|
+
|
|
72
|
+
## Choose Open or Protected Access
|
|
73
|
+
|
|
74
|
+
The starter is open by default. To protect it, pass an `access` policy and an
|
|
75
|
+
`authentication` adapter to `createBackendExample`. The ordinary tests show
|
|
76
|
+
the protected composition.
|
|
77
|
+
|
|
78
|
+
## Check This Example
|
|
79
|
+
|
|
80
|
+
Run generation drift, build, mapping, validation, and MCP checks:
|
|
81
|
+
|
|
82
|
+
```sh
|
|
83
|
+
npm run generate:check
|
|
84
|
+
npm test
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Run the language-model tool-choice and understanding check separately:
|
|
88
|
+
|
|
89
|
+
```sh
|
|
90
|
+
npm run test:llm
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
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,74 @@
|
|
|
1
|
+
# OpenAPI-Backed Pet Lookup
|
|
2
|
+
|
|
3
|
+
The `get-pet` tool looks up one pet in
|
|
4
|
+
[Swagger Petstore](https://petstore3.swagger.io/) by a positive identifier.
|
|
5
|
+
The backend path parameters, response TypeScript declarations, and response
|
|
6
|
+
validator come from the committed OpenAPI contract. The public MCP input and
|
|
7
|
+
output remain separately described and bounded by the application.
|
|
8
|
+
|
|
9
|
+
The canonical contract is
|
|
10
|
+
[`contracts/petstore.openapi.json`](contracts/petstore.openapi.json). It was
|
|
11
|
+
retrieved on 11 September 2026 from the exact
|
|
12
|
+
[Swagger Petstore OpenAPI 3 endpoint](https://petstore3.swagger.io/api/v3/openapi.json)
|
|
13
|
+
at version `1.0.27`; the committed file has SHA-256
|
|
14
|
+
`f50a32e57d10018049bcd51c28bcb47f8e595563f7583cedaa9e7134376cec62`.
|
|
15
|
+
|
|
16
|
+
The corresponding
|
|
17
|
+
[upstream source at revision `d57941e`](https://github.com/swagger-api/swagger-petstore/blob/d57941e8fe959e508796b27469b1e8bba73392dc/src/main/resources/openapi.yaml)
|
|
18
|
+
identifies the contract as Apache-2.0 licensed.
|
|
19
|
+
|
|
20
|
+
The generation script accepts only local JSON Pointer references beginning
|
|
21
|
+
with `#/`. It rejects web and relative-file references before conversion and
|
|
22
|
+
again after Swagger 2 conversion. Generation, build, tests, start, and request
|
|
23
|
+
handling never fetch a contract.
|
|
24
|
+
|
|
25
|
+
## Generate Types and Validators
|
|
26
|
+
|
|
27
|
+
Edit the local contract, then regenerate and review the diff:
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
npm run generate
|
|
31
|
+
npm run generate:check
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
The checked-in files under `src/generated/` use `typed-openapi` with Zod 4,
|
|
35
|
+
strict validation, the `getPetById` operation filter, and no generated HTTP
|
|
36
|
+
client. Normal builds use these files without running the generator.
|
|
37
|
+
|
|
38
|
+
The local
|
|
39
|
+
[`test/fixtures/petstore.swagger.yaml`](test/fixtures/petstore.swagger.yaml)
|
|
40
|
+
proves the older Swagger 2 import path. It is converted to OpenAPI 3 and sent
|
|
41
|
+
through the same generator; it is not a second application template.
|
|
42
|
+
|
|
43
|
+
## Run
|
|
44
|
+
|
|
45
|
+
```sh
|
|
46
|
+
npm install
|
|
47
|
+
npm run build
|
|
48
|
+
npm start
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
The MCP endpoint is `http://127.0.0.1:3000/mcp`.
|
|
52
|
+
|
|
53
|
+
## Choose Open or Protected Access
|
|
54
|
+
|
|
55
|
+
The starter is open by default. To protect it, pass an `access` policy and an
|
|
56
|
+
`authentication` adapter to `createBackendExample`. The ordinary tests show
|
|
57
|
+
the protected composition.
|
|
58
|
+
|
|
59
|
+
## Check This Example
|
|
60
|
+
|
|
61
|
+
Run generation drift, build, mapping, validation, and MCP checks:
|
|
62
|
+
|
|
63
|
+
```sh
|
|
64
|
+
npm run generate:check
|
|
65
|
+
npm test
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Run the language-model tool-choice and understanding check separately:
|
|
69
|
+
|
|
70
|
+
```sh
|
|
71
|
+
npm run test:llm
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
If Claude is not already signed in, run `claude auth login` first.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"openapi":"3.0.4","info":{"title":"Swagger Petstore - OpenAPI 3.0","description":"This is a sample Pet Store Server based on the OpenAPI 3.0 specification. You can find out more about\nSwagger at [https://swagger.io](https://swagger.io). In the third iteration of the pet store, we've switched to the design first approach!\nYou can now help us improve the API whether it's by making changes to the definition itself or to the code.\nThat way, with time, we can improve the API in general, and expose some of the new features in OAS3.\n\nSome useful links:\n- [The Pet Store repository](https://github.com/swagger-api/swagger-petstore)\n- [The source API definition for the Pet Store](https://github.com/swagger-api/swagger-petstore/blob/master/src/main/resources/openapi.yaml)","termsOfService":"https://swagger.io/terms/","contact":{"email":"apiteam@swagger.io"},"license":{"name":"Apache 2.0","url":"https://www.apache.org/licenses/LICENSE-2.0.html"},"version":"1.0.27"},"externalDocs":{"description":"Find out more about Swagger","url":"https://swagger.io"},"servers":[{"url":"/api/v3"}],"tags":[{"name":"pet","description":"Everything about your Pets","externalDocs":{"description":"Find out more","url":"https://swagger.io"}},{"name":"store","description":"Access to Petstore orders","externalDocs":{"description":"Find out more about our store","url":"https://swagger.io"}},{"name":"user","description":"Operations about user"}],"paths":{"/pet":{"put":{"tags":["pet"],"summary":"Update an existing pet.","description":"Update an existing pet by Id.","operationId":"updatePet","requestBody":{"description":"Update an existent pet in the store","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Pet"}},"application/xml":{"schema":{"$ref":"#/components/schemas/Pet"}},"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Pet"}}},"required":true},"responses":{"200":{"description":"Successful operation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Pet"}},"application/xml":{"schema":{"$ref":"#/components/schemas/Pet"}}}},"400":{"description":"Invalid ID supplied"},"404":{"description":"Pet not found"},"422":{"description":"Validation exception"},"default":{"description":"Unexpected error"}},"security":[{"petstore_auth":["write:pets","read:pets"]}]},"post":{"tags":["pet"],"summary":"Add a new pet to the store.","description":"Add a new pet to the store.","operationId":"addPet","requestBody":{"description":"Create a new pet in the store","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Pet"}},"application/xml":{"schema":{"$ref":"#/components/schemas/Pet"}},"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Pet"}}},"required":true},"responses":{"200":{"description":"Successful operation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Pet"}},"application/xml":{"schema":{"$ref":"#/components/schemas/Pet"}}}},"400":{"description":"Invalid input"},"422":{"description":"Validation exception"},"default":{"description":"Unexpected error"}},"security":[{"petstore_auth":["write:pets","read:pets"]}]}},"/pet/findByStatus":{"get":{"tags":["pet"],"summary":"Finds Pets by status.","description":"Multiple status values can be provided with comma separated strings.","operationId":"findPetsByStatus","parameters":[{"name":"status","in":"query","description":"Status values that need to be considered for filter","required":true,"explode":true,"schema":{"type":"string","default":"available","enum":["available","pending","sold"]}}],"responses":{"200":{"description":"successful operation","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Pet"}}},"application/xml":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Pet"}}}}},"400":{"description":"Invalid status value"},"default":{"description":"Unexpected error"}},"security":[{"petstore_auth":["write:pets","read:pets"]}]}},"/pet/findByTags":{"get":{"tags":["pet"],"summary":"Finds Pets by tags.","description":"Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing.","operationId":"findPetsByTags","parameters":[{"name":"tags","in":"query","description":"Tags to filter by","required":true,"explode":true,"schema":{"type":"array","items":{"type":"string"}}}],"responses":{"200":{"description":"successful operation","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Pet"}}},"application/xml":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Pet"}}}}},"400":{"description":"Invalid tag value"},"default":{"description":"Unexpected error"}},"security":[{"petstore_auth":["write:pets","read:pets"]}]}},"/pet/{petId}":{"get":{"tags":["pet"],"summary":"Find pet by ID.","description":"Returns a single pet.","operationId":"getPetById","parameters":[{"name":"petId","in":"path","description":"ID of pet to return","required":true,"schema":{"type":"integer","format":"int64"}}],"responses":{"200":{"description":"successful operation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Pet"}},"application/xml":{"schema":{"$ref":"#/components/schemas/Pet"}}}},"400":{"description":"Invalid ID supplied"},"404":{"description":"Pet not found"},"default":{"description":"Unexpected error"}},"security":[{"api_key":[]},{"petstore_auth":["write:pets","read:pets"]}]},"post":{"tags":["pet"],"summary":"Updates a pet in the store with form data.","description":"Updates a pet resource based on the form data.","operationId":"updatePetWithForm","parameters":[{"name":"petId","in":"path","description":"ID of pet that needs to be updated","required":true,"schema":{"type":"integer","format":"int64"}},{"name":"name","in":"query","description":"Name of pet that needs to be updated","schema":{"type":"string"}},{"name":"status","in":"query","description":"Status of pet that needs to be updated","schema":{"type":"string"}}],"responses":{"200":{"description":"successful operation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Pet"}},"application/xml":{"schema":{"$ref":"#/components/schemas/Pet"}}}},"400":{"description":"Invalid input"},"default":{"description":"Unexpected error"}},"security":[{"petstore_auth":["write:pets","read:pets"]}]},"delete":{"tags":["pet"],"summary":"Deletes a pet.","description":"Delete a pet.","operationId":"deletePet","parameters":[{"name":"api_key","in":"header","description":"","required":false,"schema":{"type":"string"}},{"name":"petId","in":"path","description":"Pet id to delete","required":true,"schema":{"type":"integer","format":"int64"}}],"responses":{"200":{"description":"Pet deleted"},"400":{"description":"Invalid pet value"},"default":{"description":"Unexpected error"}},"security":[{"petstore_auth":["write:pets","read:pets"]}]}},"/pet/{petId}/uploadImage":{"post":{"tags":["pet"],"summary":"Uploads an image.","description":"Upload image of the pet.","operationId":"uploadFile","parameters":[{"name":"petId","in":"path","description":"ID of pet to update","required":true,"schema":{"type":"integer","format":"int64"}},{"name":"additionalMetadata","in":"query","description":"Additional Metadata","required":false,"schema":{"type":"string"}}],"requestBody":{"content":{"application/octet-stream":{"schema":{"type":"string","format":"binary"}}}},"responses":{"200":{"description":"successful operation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse"}}}},"400":{"description":"No file uploaded"},"404":{"description":"Pet not found"},"default":{"description":"Unexpected error"}},"security":[{"petstore_auth":["write:pets","read:pets"]}]}},"/store/inventory":{"get":{"tags":["store"],"summary":"Returns pet inventories by status.","description":"Returns a map of status codes to quantities.","operationId":"getInventory","responses":{"200":{"description":"successful operation","content":{"application/json":{"schema":{"type":"object","additionalProperties":{"type":"integer","format":"int32"}}}}},"default":{"description":"Unexpected error"}},"security":[{"api_key":[]}]}},"/store/order":{"post":{"tags":["store"],"summary":"Place an order for a pet.","description":"Place a new order in the store.","operationId":"placeOrder","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Order"}},"application/xml":{"schema":{"$ref":"#/components/schemas/Order"}},"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Order"}}}},"responses":{"200":{"description":"successful operation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Order"}}}},"400":{"description":"Invalid input"},"422":{"description":"Validation exception"},"default":{"description":"Unexpected error"}}}},"/store/order/{orderId}":{"get":{"tags":["store"],"summary":"Find purchase order by ID.","description":"For valid response try integer IDs with value <= 5 or > 10. Other values will generate exceptions.","operationId":"getOrderById","parameters":[{"name":"orderId","in":"path","description":"ID of order that needs to be fetched","required":true,"schema":{"type":"integer","format":"int64"}}],"responses":{"200":{"description":"successful operation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Order"}},"application/xml":{"schema":{"$ref":"#/components/schemas/Order"}}}},"400":{"description":"Invalid ID supplied"},"404":{"description":"Order not found"},"default":{"description":"Unexpected error"}}},"delete":{"tags":["store"],"summary":"Delete purchase order by identifier.","description":"For valid response try integer IDs with value < 1000. Anything above 1000 or non-integers will generate API errors.","operationId":"deleteOrder","parameters":[{"name":"orderId","in":"path","description":"ID of the order that needs to be deleted","required":true,"schema":{"type":"integer","format":"int64"}}],"responses":{"200":{"description":"order deleted"},"400":{"description":"Invalid ID supplied"},"404":{"description":"Order not found"},"default":{"description":"Unexpected error"}}}},"/user":{"post":{"tags":["user"],"summary":"Create user.","description":"This can only be done by the logged in user.","operationId":"createUser","requestBody":{"description":"Created user object","content":{"application/json":{"schema":{"$ref":"#/components/schemas/User"}},"application/xml":{"schema":{"$ref":"#/components/schemas/User"}},"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/User"}}}},"responses":{"200":{"description":"successful operation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/User"}},"application/xml":{"schema":{"$ref":"#/components/schemas/User"}}}},"default":{"description":"Unexpected error"}}}},"/user/createWithList":{"post":{"tags":["user"],"summary":"Creates list of users with given input array.","description":"Creates list of users with given input array.","operationId":"createUsersWithListInput","requestBody":{"content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/User"}}}}},"responses":{"200":{"description":"Successful operation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/User"}},"application/xml":{"schema":{"$ref":"#/components/schemas/User"}}}},"default":{"description":"Unexpected error"}}}},"/user/login":{"get":{"tags":["user"],"summary":"Logs user into the system.","description":"Log into the system.","operationId":"loginUser","parameters":[{"name":"username","in":"query","description":"The user name for login","required":false,"schema":{"type":"string"}},{"name":"password","in":"query","description":"The password for login in clear text","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"successful operation","headers":{"X-Rate-Limit":{"description":"calls per hour allowed by the user","schema":{"type":"integer","format":"int32"}},"X-Expires-After":{"description":"date in UTC when token expires","schema":{"type":"string","format":"date-time"}}},"content":{"application/xml":{"schema":{"type":"string"}},"application/json":{"schema":{"type":"string"}}}},"400":{"description":"Invalid username/password supplied"},"default":{"description":"Unexpected error"}}}},"/user/logout":{"get":{"tags":["user"],"summary":"Logs out current logged in user session.","description":"Log user out of the system.","operationId":"logoutUser","parameters":[],"responses":{"200":{"description":"successful operation"},"default":{"description":"Unexpected error"}}}},"/user/{username}":{"get":{"tags":["user"],"summary":"Get user by user name.","description":"Get user detail based on username.","operationId":"getUserByName","parameters":[{"name":"username","in":"path","description":"The name that needs to be fetched. Use user1 for testing","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"successful operation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/User"}},"application/xml":{"schema":{"$ref":"#/components/schemas/User"}}}},"400":{"description":"Invalid username supplied"},"404":{"description":"User not found"},"default":{"description":"Unexpected error"}}},"put":{"tags":["user"],"summary":"Update user resource.","description":"This can only be done by the logged in user.","operationId":"updateUser","parameters":[{"name":"username","in":"path","description":"name that need to be deleted","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Update an existent user in the store","content":{"application/json":{"schema":{"$ref":"#/components/schemas/User"}},"application/xml":{"schema":{"$ref":"#/components/schemas/User"}},"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/User"}}}},"responses":{"200":{"description":"successful operation"},"400":{"description":"bad request"},"404":{"description":"user not found"},"default":{"description":"Unexpected error"}}},"delete":{"tags":["user"],"summary":"Delete user resource.","description":"This can only be done by the logged in user.","operationId":"deleteUser","parameters":[{"name":"username","in":"path","description":"The name that needs to be deleted","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"User deleted"},"400":{"description":"Invalid username supplied"},"404":{"description":"User not found"},"default":{"description":"Unexpected error"}}}}},"components":{"schemas":{"Order":{"type":"object","properties":{"id":{"type":"integer","format":"int64","example":10},"petId":{"type":"integer","format":"int64","example":198772},"quantity":{"type":"integer","format":"int32","example":7},"shipDate":{"type":"string","format":"date-time"},"status":{"type":"string","description":"Order Status","example":"approved","enum":["placed","approved","delivered"]},"complete":{"type":"boolean"}},"xml":{"name":"order"}},"Category":{"type":"object","properties":{"id":{"type":"integer","format":"int64","example":1},"name":{"type":"string","example":"Dogs"}},"xml":{"name":"category"}},"User":{"type":"object","properties":{"id":{"type":"integer","format":"int64","example":10},"username":{"type":"string","example":"theUser"},"firstName":{"type":"string","example":"John"},"lastName":{"type":"string","example":"James"},"email":{"type":"string","example":"john@email.com"},"password":{"type":"string","example":"12345"},"phone":{"type":"string","example":"12345"},"userStatus":{"type":"integer","description":"User Status","format":"int32","example":1}},"xml":{"name":"user"}},"Tag":{"type":"object","properties":{"id":{"type":"integer","format":"int64"},"name":{"type":"string"}},"xml":{"name":"tag"}},"Pet":{"required":["name","photoUrls"],"type":"object","properties":{"id":{"type":"integer","format":"int64","example":10},"name":{"type":"string","example":"doggie"},"category":{"$ref":"#/components/schemas/Category"},"photoUrls":{"type":"array","xml":{"wrapped":true},"items":{"type":"string","xml":{"name":"photoUrl"}}},"tags":{"type":"array","xml":{"wrapped":true},"items":{"$ref":"#/components/schemas/Tag"}},"status":{"type":"string","description":"pet status in the store","enum":["available","pending","sold"]}},"xml":{"name":"pet"}},"ApiResponse":{"type":"object","properties":{"code":{"type":"integer","format":"int32"},"type":{"type":"string"},"message":{"type":"string"}},"xml":{"name":"##default"}}},"requestBodies":{"Pet":{"description":"Pet object that needs to be added to the store","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Pet"}},"application/xml":{"schema":{"$ref":"#/components/schemas/Pet"}}}},"UserArray":{"description":"List of user object","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/User"}}}}}},"securitySchemes":{"petstore_auth":{"type":"oauth2","flows":{"implicit":{"authorizationUrl":"https://petstore3.swagger.io/oauth/authorize","scopes":{"write:pets":"modify pets in your account","read:pets":"read your pets"}}}},"api_key":{"type":"apiKey","name":"api_key","in":"header"}}}}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import {
|
|
3
|
+
assertNoNegativeFeedback,
|
|
4
|
+
assertResponseContains,
|
|
5
|
+
assertResponseMeaning,
|
|
6
|
+
assertToolCallsWithOptionalFeedback,
|
|
7
|
+
createConversation,
|
|
8
|
+
} from "@emseepea/testing/semantic";
|
|
9
|
+
|
|
10
|
+
test("looks up a pet and explains its status", async (t) => {
|
|
11
|
+
const chat = await createConversation(t, {
|
|
12
|
+
server: new URL("../test-support/llm-server.mjs", import.meta.url),
|
|
13
|
+
});
|
|
14
|
+
const response = await chat.send("Look up Swagger Petstore pet 7. What is its name and status?");
|
|
15
|
+
|
|
16
|
+
await assertToolCallsWithOptionalFeedback(response, [{ name: "get-pet", arguments: { petId: 7 } }]);
|
|
17
|
+
assertResponseContains(response, "Sweet Pea");
|
|
18
|
+
await assertResponseMeaning(response, { expected: "Pet 7 is named Sweet Pea and is available." });
|
|
19
|
+
assertNoNegativeFeedback(response);
|
|
20
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "emseepea-starter",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"description": "Create an Em See Pea server backed by an OpenAPI-described web API.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "tsc -p tsconfig.json",
|
|
9
|
+
"generate": "node scripts/generate.mjs",
|
|
10
|
+
"generate:check": "node scripts/check-generated.mjs",
|
|
11
|
+
"start": "node dist/server.js",
|
|
12
|
+
"test": "npm run generate:check && npm run license:check && 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 eval scripts test-support",
|
|
17
|
+
"license:check": "node scripts/check-licenses.mjs"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"@emseepea/feedback": "0.2.1",
|
|
21
|
+
"@scalar/openapi-upgrader": "0.2.15",
|
|
22
|
+
"@emseepea/testing": "0.9.4",
|
|
23
|
+
"@modelcontextprotocol/client": "2.0.0",
|
|
24
|
+
"@types/node": "24.13.3",
|
|
25
|
+
"oxlint": "1.80.0",
|
|
26
|
+
"typed-openapi": "4.0.1",
|
|
27
|
+
"typescript": "6.0.3",
|
|
28
|
+
"yaml": "2.9.0"
|
|
29
|
+
},
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=22"
|
|
32
|
+
},
|
|
33
|
+
"private": true,
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"@emseepea/server": "0.7.0",
|
|
36
|
+
"zod": "4.4.3"
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { canonicalArtifacts, generatedArtifacts, upgradeSwagger } from "./generate.mjs";
|
|
4
|
+
|
|
5
|
+
const generated = await canonicalArtifacts();
|
|
6
|
+
assert.match(generated.schemas, /from "zod"/);
|
|
7
|
+
assert.match(generated.schemas, /get_GetPetById/);
|
|
8
|
+
assert.doesNotMatch(generated.schemas, /\bfetch\s*\(/);
|
|
9
|
+
assert.match(generated.types, /namespace Endpoints/);
|
|
10
|
+
assert.equal(
|
|
11
|
+
await readFile(new URL("../src/generated/petstore.ts", import.meta.url), "utf8"),
|
|
12
|
+
generated.schemas,
|
|
13
|
+
"generated OpenAPI schemas are stale",
|
|
14
|
+
);
|
|
15
|
+
assert.equal(
|
|
16
|
+
await readFile(new URL("../src/generated/petstore.types.d.ts", import.meta.url), "utf8"),
|
|
17
|
+
generated.types,
|
|
18
|
+
"generated OpenAPI declarations are stale",
|
|
19
|
+
);
|
|
20
|
+
const swagger = upgradeSwagger(await readFile(new URL("../test/fixtures/petstore.swagger.yaml", import.meta.url), "utf8"));
|
|
21
|
+
assert.equal(swagger.paths["/pet/{petId}"].get.operationId, "getPetById");
|
|
22
|
+
assert.match((await generatedArtifacts(swagger)).schemas, /get_GetPetById/);
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { readFile } from "node:fs/promises";
|
|
4
|
+
import { createRequire } from "node:module";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
|
|
7
|
+
const require = createRequire(import.meta.url);
|
|
8
|
+
|
|
9
|
+
async function manifest(name) {
|
|
10
|
+
const root = path.dirname(path.dirname(require.resolve(name)));
|
|
11
|
+
return JSON.parse(await readFile(path.join(root, "package.json"), "utf8"));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const typedOpenapiRoot = path.dirname(path.dirname(require.resolve("typed-openapi")));
|
|
15
|
+
const typedOpenapiLicense = await readFile(path.join(typedOpenapiRoot, "LICENSE"));
|
|
16
|
+
assert.equal((await manifest("typed-openapi")).version, "4.0.1");
|
|
17
|
+
assert.equal(
|
|
18
|
+
createHash("sha256").update(typedOpenapiLicense).digest("hex"),
|
|
19
|
+
"ec37c1598e498ddfad278fc180baaf2fd2545d39754075cd0a76dbe85198c90c",
|
|
20
|
+
"typed-openapi's shipped MIT licence changed",
|
|
21
|
+
);
|
|
22
|
+
assert.deepEqual(
|
|
23
|
+
[await manifest("@scalar/openapi-upgrader"), await manifest("yaml")]
|
|
24
|
+
.map(({ name, version, license }) => ({ name, version, license })),
|
|
25
|
+
[
|
|
26
|
+
{ name: "@scalar/openapi-upgrader", version: "0.2.15", license: "MIT" },
|
|
27
|
+
{ name: "yaml", version: "2.9.0", license: "ISC" },
|
|
28
|
+
],
|
|
29
|
+
);
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { upgradeFromTwoToThree } from "@scalar/openapi-upgrader/2.0-to-3.0";
|
|
7
|
+
import { generateClientFiles } from "typed-openapi/node";
|
|
8
|
+
import { parse } from "yaml";
|
|
9
|
+
|
|
10
|
+
const contract = new URL("../contracts/petstore.openapi.json", import.meta.url);
|
|
11
|
+
|
|
12
|
+
export function assertLocalReferences(value, location = "$") {
|
|
13
|
+
if (!value || typeof value !== "object") return;
|
|
14
|
+
if ("$ref" in value) {
|
|
15
|
+
assert.equal(typeof value.$ref, "string", `${location} has a non-string $ref`);
|
|
16
|
+
assert.match(value.$ref, /^#\//, `${location} has a non-local $ref`);
|
|
17
|
+
}
|
|
18
|
+
for (const [key, child] of Object.entries(value)) {
|
|
19
|
+
assertLocalReferences(child, `${location}.${key}`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function parseContract(source) {
|
|
24
|
+
const document = parse(source);
|
|
25
|
+
assertLocalReferences(document);
|
|
26
|
+
return document;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function upgradeSwagger(source) {
|
|
30
|
+
const upgraded = upgradeFromTwoToThree(parseContract(source));
|
|
31
|
+
assertLocalReferences(upgraded);
|
|
32
|
+
return upgraded;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function generatedArtifacts(document) {
|
|
36
|
+
assertLocalReferences(document);
|
|
37
|
+
const directory = await mkdtemp(path.join(tmpdir(), "emseepea-openapi-"));
|
|
38
|
+
try {
|
|
39
|
+
const input = path.join(directory, "petstore.json");
|
|
40
|
+
const output = path.join(directory, "petstore.ts");
|
|
41
|
+
await writeFile(input, `${JSON.stringify(document)}\n`);
|
|
42
|
+
await generateClientFiles(input, {
|
|
43
|
+
output,
|
|
44
|
+
runtime: "zod",
|
|
45
|
+
validation: "strict",
|
|
46
|
+
includeClient: false,
|
|
47
|
+
includeDescriptions: true,
|
|
48
|
+
endpoint: "getPetById",
|
|
49
|
+
});
|
|
50
|
+
return {
|
|
51
|
+
schemas: normalize(await readFile(output, "utf8")),
|
|
52
|
+
types: normalize(await readFile(path.join(directory, "petstore.types.d.ts"), "utf8")),
|
|
53
|
+
};
|
|
54
|
+
} finally {
|
|
55
|
+
await rm(directory, { recursive: true, force: true });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function normalize(source) {
|
|
60
|
+
return `${source.split("\n").map((line) => line.trimEnd()).join("\n").trim()}\n`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export async function canonicalArtifacts() {
|
|
64
|
+
return generatedArtifacts(parseContract(await readFile(contract, "utf8")));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
|
|
68
|
+
const generated = await canonicalArtifacts();
|
|
69
|
+
await mkdir(new URL("../src/generated/", import.meta.url), { recursive: true });
|
|
70
|
+
await Promise.all([
|
|
71
|
+
writeFile(new URL("../src/generated/petstore.ts", import.meta.url), generated.schemas),
|
|
72
|
+
writeFile(new URL("../src/generated/petstore.types.d.ts", import.meta.url), generated.types),
|
|
73
|
+
]);
|
|
74
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createEmseepea,
|
|
3
|
+
discoverCapabilities,
|
|
4
|
+
type AccessPolicy,
|
|
5
|
+
type EmseepeaExtensions,
|
|
6
|
+
} from "@emseepea/server";
|
|
7
|
+
import type { JsonHttpClient } from "@emseepea/server/http";
|
|
8
|
+
|
|
9
|
+
export interface BackendExampleOptions extends EmseepeaExtensions {
|
|
10
|
+
readonly access?: AccessPolicy;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function createBackendExample(
|
|
14
|
+
client: JsonHttpClient,
|
|
15
|
+
options: BackendExampleOptions = {},
|
|
16
|
+
): Promise<ReturnType<typeof createEmseepea>> {
|
|
17
|
+
const { access = { access: "public" }, ...extensions } = options;
|
|
18
|
+
return createEmseepea({
|
|
19
|
+
name: "emseepea-openapi-backed-server",
|
|
20
|
+
version: "0.0.0",
|
|
21
|
+
instructions: "Use get-pet to look up a pet in Swagger Petstore by identifier.",
|
|
22
|
+
...await discoverCapabilities(new URL("./capabilities/", import.meta.url), { client, access }),
|
|
23
|
+
...extensions,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { defineMappedTool, type AccessPolicy, type CapabilityModuleFactory } from "@emseepea/server";
|
|
2
|
+
import type { JsonHttpClient } from "@emseepea/server/http";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { get_GetPetById } from "../generated/petstore.js";
|
|
5
|
+
|
|
6
|
+
export interface BackendExampleContext {
|
|
7
|
+
readonly client: JsonHttpClient;
|
|
8
|
+
readonly access: AccessPolicy;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const inputSchema = z.object({
|
|
12
|
+
petId: z.number().int().positive().max(Number.MAX_SAFE_INTEGER)
|
|
13
|
+
.describe("Positive Swagger Petstore identifier to look up."),
|
|
14
|
+
});
|
|
15
|
+
const outputSchema = z.object({
|
|
16
|
+
id: z.number().int().positive().describe("Identifier returned for the pet."),
|
|
17
|
+
name: z.string().min(1).max(200).describe("Name of the pet."),
|
|
18
|
+
status: z.enum(["available", "pending", "sold"]).optional()
|
|
19
|
+
.describe("Current Petstore availability when supplied."),
|
|
20
|
+
photoUrls: z.array(z.string().url().max(2_048)).max(10)
|
|
21
|
+
.describe("Up to ten photo URLs supplied by Petstore."),
|
|
22
|
+
source: z.literal("Swagger Petstore").describe("Data provider for this result."),
|
|
23
|
+
});
|
|
24
|
+
const backendInputSchema = z.object({
|
|
25
|
+
pathname: z.string().regex(/^\/api\/v3\/pet\/[1-9]\d*$/),
|
|
26
|
+
parameters: get_GetPetById.parameters.path,
|
|
27
|
+
});
|
|
28
|
+
const backendOutputSchema = z.object({
|
|
29
|
+
request: backendInputSchema,
|
|
30
|
+
payload: get_GetPetById.responses[200],
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
export default (({ client, access }) => defineMappedTool({
|
|
34
|
+
name: "get-pet",
|
|
35
|
+
...access,
|
|
36
|
+
description: "Look up one pet by identifier in Swagger Petstore.",
|
|
37
|
+
inputSchema,
|
|
38
|
+
outputSchema,
|
|
39
|
+
backendInputSchema,
|
|
40
|
+
backendOutputSchema,
|
|
41
|
+
mapInput: ({ petId }) => ({
|
|
42
|
+
pathname: `/api/v3/pet/${petId}`,
|
|
43
|
+
parameters: { petId },
|
|
44
|
+
}),
|
|
45
|
+
async adapter(request, { signal, deadlineMs }) {
|
|
46
|
+
return {
|
|
47
|
+
request,
|
|
48
|
+
payload: await client.get({ pathname: request.pathname, signal, deadlineMs }),
|
|
49
|
+
};
|
|
50
|
+
},
|
|
51
|
+
mapOutput: ({ request, payload }) => ({
|
|
52
|
+
data: {
|
|
53
|
+
id: payload.id ?? request.parameters.petId,
|
|
54
|
+
name: payload.name,
|
|
55
|
+
status: payload.status,
|
|
56
|
+
photoUrls: payload.photoUrls,
|
|
57
|
+
source: "Swagger Petstore" as const,
|
|
58
|
+
},
|
|
59
|
+
}),
|
|
60
|
+
})) satisfies CapabilityModuleFactory<BackendExampleContext>;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
import type * as __TypedOpenapi from "./petstore.types.js";
|
|
3
|
+
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
|
|
6
|
+
// <Schemas>
|
|
7
|
+
export type Category = __TypedOpenapi.Schemas.Category;
|
|
8
|
+
export const Category = z.object({ id: z.number().int(), name: z.string() }).partial().catchall(z.unknown());
|
|
9
|
+
|
|
10
|
+
export type Tag = __TypedOpenapi.Schemas.Tag;
|
|
11
|
+
export const Tag = z.object({ id: z.number().int(), name: z.string() }).partial().catchall(z.unknown());
|
|
12
|
+
|
|
13
|
+
export type Pet = __TypedOpenapi.Schemas.Pet;
|
|
14
|
+
export const Pet = z.object({ id: z.number().int().optional(), name: z.string(), category: Category.optional(), photoUrls: z.array(z.string()), tags: z.array(Tag).optional(), status: z.enum(["available", "pending", "sold"]).describe("pet status in the store").optional() }).catchall(z.unknown());
|
|
15
|
+
|
|
16
|
+
// </Schemas>
|
|
17
|
+
|
|
18
|
+
// <Endpoints>
|
|
19
|
+
export type get_GetPetById = __TypedOpenapi.Endpoints.get_GetPetById;
|
|
20
|
+
export const get_GetPetById = {
|
|
21
|
+
method: z.literal("GET"),
|
|
22
|
+
path: z.literal("/pet/{petId}"),
|
|
23
|
+
requestFormat: z.literal("json"),
|
|
24
|
+
responseFormat: z.literal("json"),
|
|
25
|
+
parameters: { path: z.object({ petId: z.coerce.number().int() }).strict() },
|
|
26
|
+
responses: { 200: Pet, 400: z.unknown(), 404: z.unknown(), default: z.unknown() },
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// </Endpoints>
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
// <EndpointByMethod>
|
|
33
|
+
export const EndpointByMethod = {
|
|
34
|
+
get: {
|
|
35
|
+
"/pet/{petId}": get_GetPetById
|
|
36
|
+
}
|
|
37
|
+
} satisfies { [M in keyof __TypedOpenapi.EndpointByMethod]: { [P in keyof __TypedOpenapi.EndpointByMethod[M]]: unknown } }
|
|
38
|
+
export type EndpointByMethod = __TypedOpenapi.EndpointByMethod;
|
|
39
|
+
// </EndpointByMethod>
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
// <EndpointByMethod.Shorthands>
|
|
43
|
+
export type GetEndpoints = EndpointByMethod["get"]
|
|
44
|
+
// </EndpointByMethod.Shorthands>
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
export namespace Schemas {
|
|
2
|
+
// <Schemas>
|
|
3
|
+
export type Category = Partial<{ id: number, name: string }>
|
|
4
|
+
export type Tag = Partial<{ id: number, name: string }>
|
|
5
|
+
export type Pet = ({
|
|
6
|
+
id?: number;
|
|
7
|
+
name: string;
|
|
8
|
+
category?: Category;
|
|
9
|
+
photoUrls: Array<string>;
|
|
10
|
+
tags?: Array<Tag>;
|
|
11
|
+
/**
|
|
12
|
+
* pet status in the store
|
|
13
|
+
*/
|
|
14
|
+
status?: ("available" | "pending" | "sold");
|
|
15
|
+
} & Record<string, unknown>)
|
|
16
|
+
|
|
17
|
+
// </Schemas>
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export namespace Endpoints {
|
|
21
|
+
// <Endpoints>
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Returns a single pet.
|
|
25
|
+
*/
|
|
26
|
+
export type get_GetPetById = {
|
|
27
|
+
method: "GET",
|
|
28
|
+
path: "/pet/{petId}",
|
|
29
|
+
requestFormat: "json",
|
|
30
|
+
responseFormat: "json",
|
|
31
|
+
parameters: {
|
|
32
|
+
|
|
33
|
+
path: { petId: number },
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
}
|
|
38
|
+
responses: {200: Schemas.Pet,
|
|
39
|
+
400: unknown,
|
|
40
|
+
404: unknown,
|
|
41
|
+
default: unknown,
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// </Endpoints>
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
// <EndpointByMethod>
|
|
51
|
+
export type EndpointByMethod = {
|
|
52
|
+
get: {
|
|
53
|
+
"/pet/{petId}": Endpoints.get_GetPetById
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// </EndpointByMethod>
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
// <EndpointByMethod.Shorthands>
|
|
61
|
+
export type GetEndpoints = EndpointByMethod["get"]
|
|
62
|
+
// </EndpointByMethod.Shorthands>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { serveEmseepea } from "@emseepea/server";
|
|
2
|
+
import { createJsonHttpClient } from "@emseepea/server/http";
|
|
3
|
+
import { createBackendExample } from "./app.js";
|
|
4
|
+
|
|
5
|
+
const client = createJsonHttpClient({
|
|
6
|
+
origin: "https://petstore3.swagger.io",
|
|
7
|
+
maxResponseBytes: 128 * 1024,
|
|
8
|
+
});
|
|
9
|
+
const running = await serveEmseepea(
|
|
10
|
+
await createBackendExample(client),
|
|
11
|
+
{ port: Number.parseInt(process.env.PORT ?? "3000", 10) },
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
console.log(`Em See Pea OpenAPI-backed server example listening at ${running.url}`);
|
|
15
|
+
|
|
16
|
+
async function shutdown(): Promise<void> {
|
|
17
|
+
await running.close();
|
|
18
|
+
process.exitCode = 0;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
process.once("SIGINT", () => void shutdown());
|
|
22
|
+
process.once("SIGTERM", () => void shutdown());
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
swagger: "2.0"
|
|
2
|
+
info:
|
|
3
|
+
title: Petstore fixture
|
|
4
|
+
version: "1.0.0"
|
|
5
|
+
basePath: /v2
|
|
6
|
+
schemes: [https]
|
|
7
|
+
paths:
|
|
8
|
+
/pet/{petId}:
|
|
9
|
+
get:
|
|
10
|
+
operationId: getPetById
|
|
11
|
+
parameters:
|
|
12
|
+
- name: petId
|
|
13
|
+
in: path
|
|
14
|
+
required: true
|
|
15
|
+
type: integer
|
|
16
|
+
format: int64
|
|
17
|
+
minimum: 1
|
|
18
|
+
responses:
|
|
19
|
+
"200":
|
|
20
|
+
description: successful operation
|
|
21
|
+
schema:
|
|
22
|
+
$ref: "#/definitions/Pet"
|
|
23
|
+
definitions:
|
|
24
|
+
Pet:
|
|
25
|
+
type: object
|
|
26
|
+
required: [id, name, photoUrls]
|
|
27
|
+
properties:
|
|
28
|
+
id:
|
|
29
|
+
type: integer
|
|
30
|
+
format: int64
|
|
31
|
+
name:
|
|
32
|
+
type: string
|
|
33
|
+
photoUrls:
|
|
34
|
+
type: array
|
|
35
|
+
items: { type: string }
|
|
36
|
+
status:
|
|
37
|
+
type: string
|
|
38
|
+
enum: [available, pending, sold]
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { Client, StreamableHTTPClientTransport } from "@modelcontextprotocol/client";
|
|
4
|
+
import { createEmseepea, defineMappedTool, serveEmseepea } from "@emseepea/server";
|
|
5
|
+
import { insecureTestAuthentication, startEmseepea } from "@emseepea/testing";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import { createBackendExample } from "../dist/app.js";
|
|
8
|
+
import { assertLocalReferences, generatedArtifacts } from "../scripts/generate.mjs";
|
|
9
|
+
import { get_GetPetById, Pet } from "../dist/generated/petstore.js";
|
|
10
|
+
import { petstoreFixture } from "../test-support/petstore-fixture.mjs";
|
|
11
|
+
|
|
12
|
+
test("the OpenAPI-backed example uses generated request and response validation", async () => {
|
|
13
|
+
const requests = [];
|
|
14
|
+
let response = petstoreFixture;
|
|
15
|
+
const app = await createBackendExample({
|
|
16
|
+
async get(options) {
|
|
17
|
+
requests.push(options);
|
|
18
|
+
if (response instanceof Error) throw response;
|
|
19
|
+
return response;
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
const running = await serveEmseepea(app, { port: 0 });
|
|
23
|
+
const client = new Client(
|
|
24
|
+
{ name: "openapi-backed-example-test", version: "0.0.0" },
|
|
25
|
+
{ versionNegotiation: { mode: { pin: "2026-07-28" } } },
|
|
26
|
+
);
|
|
27
|
+
await client.connect(new StreamableHTTPClientTransport(new URL(running.url)));
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
const listed = await client.listTools();
|
|
31
|
+
assert.deepEqual(listed.tools.map(({ name }) => name), ["get-pet"]);
|
|
32
|
+
|
|
33
|
+
const invalidInput = await client.callTool({ name: "get-pet", arguments: { petId: 0 } });
|
|
34
|
+
assert.equal(invalidInput.isError, true);
|
|
35
|
+
assert.equal(requests.length, 0);
|
|
36
|
+
|
|
37
|
+
const result = await client.callTool({ name: "get-pet", arguments: { petId: 7 } });
|
|
38
|
+
assert.equal(result.isError, false);
|
|
39
|
+
assert.deepEqual(result.structuredContent, {
|
|
40
|
+
id: 7,
|
|
41
|
+
name: "Sweet Pea",
|
|
42
|
+
status: "available",
|
|
43
|
+
photoUrls: ["https://example.test/sweet-pea.jpg"],
|
|
44
|
+
source: "Swagger Petstore",
|
|
45
|
+
});
|
|
46
|
+
assert.equal(requests[0].pathname, "/api/v3/pet/7");
|
|
47
|
+
assert.equal(requests[0].signal instanceof AbortSignal, true);
|
|
48
|
+
assert.ok(requests[0].deadlineMs > Date.now());
|
|
49
|
+
|
|
50
|
+
response = { ...petstoreFixture, name: 42 };
|
|
51
|
+
const invalidProviderData = await client.callTool({ name: "get-pet", arguments: { petId: 7 } });
|
|
52
|
+
assert.equal(invalidProviderData.isError, true);
|
|
53
|
+
assert.doesNotMatch(JSON.stringify(invalidProviderData), /42|photoUrls/);
|
|
54
|
+
|
|
55
|
+
response = { ...petstoreFixture, private_note: "do not expose" };
|
|
56
|
+
const extraProviderData = await client.callTool({ name: "get-pet", arguments: { petId: 7 } });
|
|
57
|
+
assert.equal(extraProviderData.isError, false);
|
|
58
|
+
assert.equal("private_note" in extraProviderData.structuredContent, false);
|
|
59
|
+
} finally {
|
|
60
|
+
await client.close();
|
|
61
|
+
await running.close();
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("contract loading permits only fragment references", () => {
|
|
66
|
+
assert.doesNotThrow(() => assertLocalReferences({ $ref: "#/components/schemas/Pet" }));
|
|
67
|
+
assert.throws(() => assertLocalReferences({ $ref: "https://example.test/pet.yaml" }), /non-local/);
|
|
68
|
+
assert.throws(() => assertLocalReferences({ $ref: "pet.yaml#/Pet" }), /non-local/);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("the generation entry point rejects non-local references", async () => {
|
|
72
|
+
await assert.rejects(
|
|
73
|
+
generatedArtifacts({ $ref: "https://example.test/pet.yaml" }),
|
|
74
|
+
/non-local/,
|
|
75
|
+
);
|
|
76
|
+
await assert.rejects(generatedArtifacts({ $ref: "pet.yaml#/Pet" }), /non-local/);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("contract changes alter the generated declarations and validators", async () => {
|
|
80
|
+
const source = generationContract();
|
|
81
|
+
const baseline = await generatedArtifacts(source);
|
|
82
|
+
for (const mutate of [
|
|
83
|
+
(document) => { document.components.schemas.Pet.required = ["photoUrls"]; },
|
|
84
|
+
(document) => { delete document.components.schemas.Pet.properties.note; },
|
|
85
|
+
(document) => { document.components.schemas.Pet.properties.name.type = "integer"; },
|
|
86
|
+
(document) => { document.components.schemas.Pet.properties.status.enum.push("archived"); },
|
|
87
|
+
]) {
|
|
88
|
+
const changed = structuredClone(source);
|
|
89
|
+
mutate(changed);
|
|
90
|
+
const regenerated = await generatedArtifacts(changed);
|
|
91
|
+
assert.notEqual(regenerated.schemas, baseline.schemas);
|
|
92
|
+
assert.notEqual(regenerated.types, baseline.types);
|
|
93
|
+
}
|
|
94
|
+
assert.equal(Pet.safeParse(petstoreFixture).success, true);
|
|
95
|
+
assert.equal(Pet.safeParse({ ...petstoreFixture, status: "archived" }).success, false);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("an invalid mapped backend request makes no adapter call", async () => {
|
|
99
|
+
let adapterCalls = 0;
|
|
100
|
+
const tool = defineMappedTool({
|
|
101
|
+
name: "invalid-generated-request",
|
|
102
|
+
access: "public",
|
|
103
|
+
description: "Exercise the generated request boundary.",
|
|
104
|
+
inputSchema: z.object({ petId: z.number().int().positive() }),
|
|
105
|
+
outputSchema: z.object({ ok: z.boolean() }),
|
|
106
|
+
backendInputSchema: get_GetPetById.parameters.path,
|
|
107
|
+
backendOutputSchema: z.object({ ok: z.boolean() }),
|
|
108
|
+
mapInput: () => ({ petId: "not-an-integer" }),
|
|
109
|
+
adapter() {
|
|
110
|
+
adapterCalls += 1;
|
|
111
|
+
return { ok: true };
|
|
112
|
+
},
|
|
113
|
+
mapOutput: (data) => ({ data }),
|
|
114
|
+
});
|
|
115
|
+
const running = await serveEmseepea(createEmseepea({
|
|
116
|
+
name: "invalid-generated-request-test",
|
|
117
|
+
version: "0.0.0",
|
|
118
|
+
tools: [tool],
|
|
119
|
+
}), { port: 0 });
|
|
120
|
+
const client = new Client(
|
|
121
|
+
{ name: "invalid-generated-request-test", version: "0.0.0" },
|
|
122
|
+
{ versionNegotiation: { mode: { pin: "2026-07-28" } } },
|
|
123
|
+
);
|
|
124
|
+
await client.connect(new StreamableHTTPClientTransport(new URL(running.url)));
|
|
125
|
+
try {
|
|
126
|
+
const result = await client.callTool({ name: "invalid-generated-request", arguments: { petId: 7 } });
|
|
127
|
+
assert.equal(result.isError, true);
|
|
128
|
+
assert.equal(adapterCalls, 0);
|
|
129
|
+
} finally {
|
|
130
|
+
await client.close();
|
|
131
|
+
await running.close();
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("the same template composes protected access and observability", async (t) => {
|
|
136
|
+
const events = [];
|
|
137
|
+
const permissions = ["pets:read"];
|
|
138
|
+
const app = await createBackendExample(
|
|
139
|
+
{ get: async () => petstoreFixture },
|
|
140
|
+
{
|
|
141
|
+
access: { access: "protected", requiredScopes: permissions },
|
|
142
|
+
authentication: insecureTestAuthentication(permissions),
|
|
143
|
+
observability: [{ id: "test-log", emit: (event) => events.push(event) }],
|
|
144
|
+
},
|
|
145
|
+
);
|
|
146
|
+
const running = await startEmseepea(t, app);
|
|
147
|
+
const client = await running.connect("test-token");
|
|
148
|
+
const result = await client.callTool({ name: "get-pet", arguments: { petId: 7 } });
|
|
149
|
+
assert.equal(result.isError, false);
|
|
150
|
+
assert.ok(events.some(({ capability }) => capability === "get-pet"));
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
function generationContract() {
|
|
154
|
+
return {
|
|
155
|
+
openapi: "3.0.4",
|
|
156
|
+
info: { title: "Generation fixture", version: "1.0.0" },
|
|
157
|
+
paths: {
|
|
158
|
+
"/pet/{petId}": {
|
|
159
|
+
get: {
|
|
160
|
+
operationId: "getPetById",
|
|
161
|
+
parameters: [{
|
|
162
|
+
name: "petId",
|
|
163
|
+
in: "path",
|
|
164
|
+
required: true,
|
|
165
|
+
schema: { type: "integer", minimum: 1 },
|
|
166
|
+
}],
|
|
167
|
+
responses: {
|
|
168
|
+
200: {
|
|
169
|
+
description: "Pet",
|
|
170
|
+
content: { "application/json": { schema: { $ref: "#/components/schemas/Pet" } } },
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
components: {
|
|
177
|
+
schemas: {
|
|
178
|
+
Pet: {
|
|
179
|
+
type: "object",
|
|
180
|
+
required: ["name", "photoUrls"],
|
|
181
|
+
properties: {
|
|
182
|
+
name: { type: "string" },
|
|
183
|
+
note: { type: "string" },
|
|
184
|
+
photoUrls: { type: "array", items: { type: "string" } },
|
|
185
|
+
status: { type: "string", enum: ["available", "pending", "sold"] },
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { serveEmseepea } from "@emseepea/server";
|
|
3
|
+
import { defineFeedbackSubmission } from "@emseepea/feedback";
|
|
4
|
+
import { createBackendExample } from "../dist/app.js";
|
|
5
|
+
import { petstoreFixture } from "./petstore-fixture.mjs";
|
|
6
|
+
|
|
7
|
+
const client = {
|
|
8
|
+
async get({ pathname }) {
|
|
9
|
+
assert.equal(pathname, "/api/v3/pet/7");
|
|
10
|
+
return petstoreFixture;
|
|
11
|
+
},
|
|
12
|
+
};
|
|
13
|
+
const feedback = defineFeedbackSubmission({
|
|
14
|
+
access: "public",
|
|
15
|
+
backend: { submit: () => ({ id: crypto.randomUUID(), recordedAt: new Date().toISOString() }) },
|
|
16
|
+
});
|
|
17
|
+
const app = await createBackendExample(client, { additionalTools: [feedback] });
|
|
18
|
+
const running = await serveEmseepea(app, { port: 0 });
|
|
19
|
+
|
|
20
|
+
console.log(`Em See Pea OpenAPI-backed fixture listening at ${running.url}`);
|
|
21
|
+
|
|
22
|
+
async function shutdown() {
|
|
23
|
+
await running.close();
|
|
24
|
+
process.exitCode = 0;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
process.once("SIGINT", () => void shutdown());
|
|
28
|
+
process.once("SIGTERM", () => void shutdown());
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"module": "NodeNext",
|
|
4
|
+
"moduleResolution": "NodeNext",
|
|
5
|
+
"outDir": "dist",
|
|
6
|
+
"rootDir": "src",
|
|
7
|
+
"strict": true,
|
|
8
|
+
"target": "ES2023",
|
|
9
|
+
"types": ["node"],
|
|
10
|
+
"verbatimModuleSyntax": true
|
|
11
|
+
},
|
|
12
|
+
"include": ["src/**/*.ts"]
|
|
13
|
+
}
|
package/package.json
CHANGED
|
@@ -1,13 +1,43 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@emseepea/create-openapi-backed-server",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Create an Em See Pea server backed by an OpenAPI-described web API.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
|
-
"
|
|
8
|
-
"
|
|
9
|
-
"
|
|
10
|
-
"
|
|
11
|
-
"
|
|
12
|
-
|
|
7
|
+
"starterDependencies": ["@emseepea/server", "zod"],
|
|
8
|
+
"scripts": {
|
|
9
|
+
"build": "npm run build:example && npm run build:initializer",
|
|
10
|
+
"build:example": "tsc -p tsconfig.json",
|
|
11
|
+
"build:initializer": "node ../../scripts/build-initializer.mjs",
|
|
12
|
+
"generate": "node scripts/generate.mjs",
|
|
13
|
+
"generate:check": "node scripts/check-generated.mjs",
|
|
14
|
+
"start": "node dist/server.js",
|
|
15
|
+
"test": "npm run generate:check && npm run license:check && npm run build && npm run test:built",
|
|
16
|
+
"test:built": "node --test test/*.test.mjs",
|
|
17
|
+
"test:llm": "npm run build && npm run test:llm:built",
|
|
18
|
+
"test:llm:built": "emseepea-test eval",
|
|
19
|
+
"lint": "oxlint src test eval scripts test-support",
|
|
20
|
+
"license:check": "node scripts/check-licenses.mjs",
|
|
21
|
+
"prepack": "npm run build:initializer"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@emseepea/feedback": "0.2.1",
|
|
25
|
+
"@emseepea/server": "0.7.0",
|
|
26
|
+
"@scalar/openapi-upgrader": "0.2.15",
|
|
27
|
+
"@emseepea/testing": "0.9.4",
|
|
28
|
+
"@modelcontextprotocol/client": "2.0.0",
|
|
29
|
+
"@types/node": "24.13.3",
|
|
30
|
+
"oxlint": "1.80.0",
|
|
31
|
+
"typed-openapi": "4.0.1",
|
|
32
|
+
"typescript": "6.0.3",
|
|
33
|
+
"yaml": "2.9.0",
|
|
34
|
+
"zod": "4.4.3"
|
|
35
|
+
},
|
|
36
|
+
"engines": { "node": ">=22" },
|
|
37
|
+
"repository": { "type": "git", "url": "git+https://github.com/emseepea/emseepea.git", "directory": "examples/openapi-backed-server" },
|
|
38
|
+
"homepage": "https://emseepea.github.io/emseepea/examples/",
|
|
39
|
+
"bugs": "https://github.com/emseepea/emseepea/issues",
|
|
40
|
+
"publishConfig": { "access": "public", "provenance": true },
|
|
41
|
+
"bin": { "create-openapi-backed-server": "./initializer-dist/create.mjs" },
|
|
42
|
+
"files": ["initializer-dist"]
|
|
13
43
|
}
|