@emseepea/create-api-backed-server 0.0.0 → 0.0.2

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.
@@ -14,7 +14,7 @@
14
14
  "lint": "oxlint src test eval test-support"
15
15
  },
16
16
  "dependencies": {
17
- "@emseepea/server": "0.0.4",
17
+ "@emseepea/server": "0.1.0",
18
18
  "zod": "4.4.3"
19
19
  },
20
20
  "devDependencies": {
@@ -1,157 +1,11 @@
1
- import { createEmseepea, defineMappedTool } from "@emseepea/server";
1
+ import { createEmseepea, discoverCapabilities } from "@emseepea/server";
2
2
  import type { JsonHttpClient } from "@emseepea/server/http";
3
- import { z } from "zod";
4
-
5
- const publicRoast = z.enum(["light", "medium-light", "medium", "medium-dark", "dark"]);
6
- const brewmarkRoast = z.enum(["LIGHT", "MEDIUM_LIGHT", "MEDIUM", "MEDIUM_DARK", "DARK"]);
7
- const publicToBrewmarkRoast = {
8
- light: "LIGHT",
9
- "medium-light": "MEDIUM_LIGHT",
10
- medium: "MEDIUM",
11
- "medium-dark": "MEDIUM_DARK",
12
- dark: "DARK",
13
- } as const;
14
- const brewmarkToPublicRoast = {
15
- LIGHT: "light",
16
- MEDIUM_LIGHT: "medium-light",
17
- MEDIUM: "medium",
18
- MEDIUM_DARK: "medium-dark",
19
- DARK: "dark",
20
- } as const;
21
-
22
- const searchInput = z.object({
23
- query: z.string().trim().min(2).max(80),
24
- roast: publicRoast.optional(),
25
- });
26
- const coffee = z.object({
27
- name: z.string().max(200),
28
- roaster: z.string().max(200),
29
- origin: z.string().max(200).nullable(),
30
- roast: publicRoast,
31
- processingMethod: z.string().max(100).nullable(),
32
- flavourNotes: z.string().max(500).nullable(),
33
- acidityLevel: z.number().int().min(1).max(5).nullable(),
34
- bodyLevel: z.number().int().min(1).max(5).nullable(),
35
- });
36
- const searchReport = z.object({
37
- query: z.string().max(80),
38
- roastFilter: publicRoast.nullable(),
39
- returnedCount: z.number().int().min(0).max(5),
40
- moreMatchesAvailable: z.boolean(),
41
- ratingScale: z.object({
42
- acidity: z.literal("1 = low acidity; 5 = high acidity"),
43
- body: z.literal("1 = light body; 5 = full body"),
44
- }),
45
- coffees: z.array(coffee).max(5),
46
- source: z.literal("BrewMark"),
47
- sourceUrl: z.literal("https://brewmark.io"),
48
- });
49
-
50
- const backendCommand = z.object({
51
- pathname: z.literal("/api/coffees"),
52
- searchParams: z.object({
53
- q: z.string().min(2).max(80),
54
- roastLevel: brewmarkRoast.optional(),
55
- sort: z.literal("alpha"),
56
- limit: z.literal("5"),
57
- }),
58
- });
59
- const backendCoffee = z.object({
60
- name: z.string().max(200),
61
- roasterName: z.string().max(200),
62
- roastLevel: brewmarkRoast,
63
- origin: z.string().max(200).nullable(),
64
- processingMethod: z.string().max(100).nullable(),
65
- flavorProfile: z.string().max(500).nullable(),
66
- acidityLevel: z.number().int().min(1).max(5).nullable(),
67
- bodyLevel: z.number().int().min(1).max(5).nullable(),
68
- });
69
- const backendPayload = z.object({
70
- data: z.array(backendCoffee).max(5),
71
- cursor: z.string().max(2_048).nullable(),
72
- hasMore: z.boolean(),
73
- });
74
- const backendResult = z.object({
75
- request: backendCommand,
76
- payload: backendPayload,
77
- });
78
-
79
- export function createBackendExample(client: JsonHttpClient): ReturnType<typeof createEmseepea> {
80
- const searchCoffeeCatalog = defineMappedTool({
81
- name: "search-coffee-catalog",
82
- access: "public",
83
- description: "Search BrewMark's public coffee catalogue and explain its acidity and body ratings.",
84
- inputSchema: searchInput,
85
- outputSchema: searchReport,
86
- backendInputSchema: backendCommand,
87
- backendOutputSchema: backendResult,
88
- mapInput: ({ query, roast }) => ({
89
- pathname: "/api/coffees" as const,
90
- searchParams: {
91
- q: query,
92
- ...(roast ? { roastLevel: publicToBrewmarkRoast[roast] } : {}),
93
- sort: "alpha" as const,
94
- limit: "5" as const,
95
- },
96
- }),
97
- async adapter(request, { signal, deadlineMs }) {
98
- return {
99
- request,
100
- payload: await client.get({ ...request, signal, deadlineMs }),
101
- };
102
- },
103
- mapOutput: ({ request, payload }) => {
104
- const coffees = payload.data.map((record) => ({
105
- name: record.name,
106
- roaster: record.roasterName,
107
- origin: record.origin,
108
- roast: brewmarkToPublicRoast[record.roastLevel],
109
- processingMethod: record.processingMethod,
110
- flavourNotes: record.flavorProfile,
111
- acidityLevel: record.acidityLevel,
112
- bodyLevel: record.bodyLevel,
113
- }));
114
- const roastFilter = request.searchParams.roastLevel
115
- ? brewmarkToPublicRoast[request.searchParams.roastLevel]
116
- : null;
117
- const data = {
118
- query: request.searchParams.q,
119
- roastFilter,
120
- returnedCount: coffees.length,
121
- moreMatchesAvailable: payload.hasMore,
122
- ratingScale: {
123
- acidity: "1 = low acidity; 5 = high acidity" as const,
124
- body: "1 = light body; 5 = full body" as const,
125
- },
126
- coffees,
127
- source: "BrewMark" as const,
128
- sourceUrl: "https://brewmark.io" as const,
129
- };
130
- const lines = coffees.map((record) => [
131
- `${record.name} by ${record.roaster}`,
132
- `origin: ${record.origin ?? "not provided"}`,
133
- `roast: ${record.roast}`,
134
- `acidity: ${record.acidityLevel ?? "not provided"}`,
135
- `body: ${record.bodyLevel ?? "not provided"}`,
136
- ].join("; "));
137
- return {
138
- text: [
139
- `BrewMark returned ${coffees.length} coffee${coffees.length === 1 ? "" : "s"} for “${data.query}”.`,
140
- `More matches available: ${data.moreMatchesAvailable ? "yes" : "no"}.`,
141
- "Acidity: 1 = low acidity; 5 = high acidity.",
142
- "Body: 1 = light body; 5 = full body.",
143
- ...lines,
144
- "Source: https://brewmark.io",
145
- ].join("\n"),
146
- data,
147
- };
148
- },
149
- });
150
3
 
4
+ export async function createBackendExample(client: JsonHttpClient): Promise<ReturnType<typeof createEmseepea>> {
151
5
  return createEmseepea({
152
6
  name: "emseepea-backend-no-ui",
153
7
  version: "0.0.0",
154
8
  instructions: "Use search-coffee-catalog to search BrewMark's public coffee catalogue.",
155
- tools: [searchCoffeeCatalog],
9
+ ...await discoverCapabilities(new URL("./capabilities/", import.meta.url), { client }),
156
10
  });
157
11
  }
@@ -0,0 +1,112 @@
1
+ import { defineMappedTool, type CapabilityModuleFactory } from "@emseepea/server";
2
+ import type { JsonHttpClient } from "@emseepea/server/http";
3
+ import { z } from "zod";
4
+
5
+ export interface BackendExampleContext { readonly client: JsonHttpClient }
6
+
7
+ const publicRoast = z.enum(["light", "medium-light", "medium", "medium-dark", "dark"]);
8
+ const brewmarkRoast = z.enum(["LIGHT", "MEDIUM_LIGHT", "MEDIUM", "MEDIUM_DARK", "DARK"]);
9
+ const publicToBrewmarkRoast = {
10
+ light: "LIGHT", "medium-light": "MEDIUM_LIGHT", medium: "MEDIUM",
11
+ "medium-dark": "MEDIUM_DARK", dark: "DARK",
12
+ } as const;
13
+ const brewmarkToPublicRoast = {
14
+ LIGHT: "light", MEDIUM_LIGHT: "medium-light", MEDIUM: "medium",
15
+ MEDIUM_DARK: "medium-dark", DARK: "dark",
16
+ } as const;
17
+ const searchInput = z.object({ query: z.string().trim().min(2).max(80), roast: publicRoast.optional() });
18
+ const coffee = z.object({
19
+ name: z.string().max(200), roaster: z.string().max(200), origin: z.string().max(200).nullable(),
20
+ roast: publicRoast, processingMethod: z.string().max(100).nullable(),
21
+ flavourNotes: z.string().max(500).nullable(), acidityLevel: z.number().int().min(1).max(5).nullable(),
22
+ bodyLevel: z.number().int().min(1).max(5).nullable(),
23
+ });
24
+ const searchReport = z.object({
25
+ query: z.string().max(80), roastFilter: publicRoast.nullable(), returnedCount: z.number().int().min(0).max(5),
26
+ moreMatchesAvailable: z.boolean(),
27
+ ratingScale: z.object({
28
+ acidity: z.literal("1 = low acidity; 5 = high acidity"),
29
+ body: z.literal("1 = light body; 5 = full body"),
30
+ }),
31
+ coffees: z.array(coffee).max(5), source: z.literal("BrewMark"), sourceUrl: z.literal("https://brewmark.io"),
32
+ });
33
+ const backendCommand = z.object({
34
+ pathname: z.literal("/api/coffees"),
35
+ searchParams: z.object({
36
+ q: z.string().min(2).max(80), roastLevel: brewmarkRoast.optional(),
37
+ sort: z.literal("alpha"), limit: z.literal("5"),
38
+ }),
39
+ });
40
+ const backendCoffee = z.object({
41
+ name: z.string().max(200), roasterName: z.string().max(200), roastLevel: brewmarkRoast,
42
+ origin: z.string().max(200).nullable(), processingMethod: z.string().max(100).nullable(),
43
+ flavorProfile: z.string().max(500).nullable(), acidityLevel: z.number().int().min(1).max(5).nullable(),
44
+ bodyLevel: z.number().int().min(1).max(5).nullable(),
45
+ });
46
+ const backendPayload = z.object({
47
+ data: z.array(backendCoffee).max(5), cursor: z.string().max(2_048).nullable(), hasMore: z.boolean(),
48
+ });
49
+ const backendResult = z.object({ request: backendCommand, payload: backendPayload });
50
+
51
+ export default (({ client }) => defineMappedTool({
52
+ name: "search-coffee-catalog",
53
+ access: "public",
54
+ description: "Search BrewMark's public coffee catalogue and explain its acidity and body ratings.",
55
+ inputSchema: searchInput,
56
+ outputSchema: searchReport,
57
+ backendInputSchema: backendCommand,
58
+ backendOutputSchema: backendResult,
59
+ mapInput: ({ query, roast }) => ({
60
+ pathname: "/api/coffees" as const,
61
+ searchParams: {
62
+ q: query,
63
+ ...(roast ? { roastLevel: publicToBrewmarkRoast[roast] } : {}),
64
+ sort: "alpha" as const,
65
+ limit: "5" as const,
66
+ },
67
+ }),
68
+ async adapter(request, { signal, deadlineMs }) {
69
+ return { request, payload: await client.get({ ...request, signal, deadlineMs }) };
70
+ },
71
+ mapOutput: ({ request, payload }) => {
72
+ const coffees = payload.data.map((record) => ({
73
+ name: record.name, roaster: record.roasterName, origin: record.origin,
74
+ roast: brewmarkToPublicRoast[record.roastLevel], processingMethod: record.processingMethod,
75
+ flavourNotes: record.flavorProfile, acidityLevel: record.acidityLevel, bodyLevel: record.bodyLevel,
76
+ }));
77
+ const roastFilter = request.searchParams.roastLevel
78
+ ? brewmarkToPublicRoast[request.searchParams.roastLevel]
79
+ : null;
80
+ const data = {
81
+ query: request.searchParams.q,
82
+ roastFilter,
83
+ returnedCount: coffees.length,
84
+ moreMatchesAvailable: payload.hasMore,
85
+ ratingScale: {
86
+ acidity: "1 = low acidity; 5 = high acidity" as const,
87
+ body: "1 = light body; 5 = full body" as const,
88
+ },
89
+ coffees,
90
+ source: "BrewMark" as const,
91
+ sourceUrl: "https://brewmark.io" as const,
92
+ };
93
+ const lines = coffees.map((record) => [
94
+ `${record.name} by ${record.roaster}`,
95
+ `origin: ${record.origin ?? "not provided"}`,
96
+ `roast: ${record.roast}`,
97
+ `acidity: ${record.acidityLevel ?? "not provided"}`,
98
+ `body: ${record.bodyLevel ?? "not provided"}`,
99
+ ].join("; "));
100
+ return {
101
+ text: [
102
+ `BrewMark returned ${coffees.length} coffee${coffees.length === 1 ? "" : "s"} for “${data.query}”.`,
103
+ `More matches available: ${data.moreMatchesAvailable ? "yes" : "no"}.`,
104
+ "Acidity: 1 = low acidity; 5 = high acidity.",
105
+ "Body: 1 = light body; 5 = full body.",
106
+ ...lines,
107
+ "Source: https://brewmark.io",
108
+ ].join("\n"),
109
+ data,
110
+ };
111
+ },
112
+ })) satisfies CapabilityModuleFactory<BackendExampleContext>;
@@ -7,7 +7,7 @@ const client = createJsonHttpClient({
7
7
  maxResponseBytes: 128 * 1024,
8
8
  });
9
9
  const running = await serveEmseepea(
10
- createBackendExample(client),
10
+ await createBackendExample(client),
11
11
  { port: Number.parseInt(process.env.PORT ?? "3000", 10) },
12
12
  );
13
13
 
@@ -8,7 +8,7 @@ import { brewmarkFixture } from "../test-support/brewmark-fixture.mjs";
8
8
  test("the backend example maps, checks, and explains BrewMark data", async () => {
9
9
  const requests = [];
10
10
  let response = brewmarkFixture;
11
- const app = createBackendExample({
11
+ const app = await createBackendExample({
12
12
  async get(options) {
13
13
  requests.push(options);
14
14
  if (response instanceof Error) throw response;
@@ -3,7 +3,7 @@ import { serveEmseepea } from "@emseepea/server";
3
3
  import { createBackendExample } from "../dist/app.js";
4
4
  import { brewmarkFixture } from "./brewmark-fixture.mjs";
5
5
 
6
- const app = createBackendExample({
6
+ const app = await createBackendExample({
7
7
  async get({ pathname, searchParams }) {
8
8
  assert.equal(pathname, "/api/coffees");
9
9
  const { q, ...options } = searchParams;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@emseepea/create-api-backed-server",
3
- "version": "0.0.0",
3
+ "version": "0.0.2",
4
4
  "description": "Create an Em See Pea server backed by a public web API.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -10,19 +10,13 @@
10
10
  },
11
11
  "homepage": "https://emseepea.github.io/emseepea/examples/",
12
12
  "bugs": "https://github.com/emseepea/emseepea/issues",
13
- "publishConfig": {
14
- "access": "public",
15
- "provenance": false,
16
- "tag": "next"
17
- },
13
+ "publishConfig": { "access": "public", "provenance": true, "tag": "next" },
18
14
  "type": "module",
19
- "bin": {
20
- "create-api-backed-server": "./dist/create.mjs"
15
+ "bin": { "create-api-backed-server": "./dist/create.mjs" },
16
+ "files": ["dist"],
17
+ "scripts": {
18
+ "build": "node ../../scripts/build-initializer.mjs",
19
+ "prepack": "npm run build"
21
20
  },
22
- "files": [
23
- "dist"
24
- ],
25
- "engines": {
26
- "node": ">=22"
27
- }
21
+ "engines": { "node": ">=22" }
28
22
  }