@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 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,8 @@
1
+ export const petstoreFixture = {
2
+ id: 7,
3
+ category: { id: 1, name: "peas" },
4
+ name: "Sweet Pea",
5
+ photoUrls: ["https://example.test/sweet-pea.jpg"],
6
+ tags: [{ id: 2, name: "flower" }],
7
+ status: "available",
8
+ };
@@ -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.0",
4
- "description": "Bootstrap-only placeholder for npm trusted publishing. Do not install.",
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
- "private": false,
8
- "repository": {
9
- "type": "git",
10
- "url": "git+https://github.com/emseepea/emseepea.git",
11
- "directory": "examples/openapi-backed-server"
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
  }