@emseepea/create-api-backed-server 0.0.1 → 0.0.3
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 +58 -11
- package/{dist → initializer-dist}/template/README.md +6 -1
- package/{dist → initializer-dist}/template/package.json +6 -5
- package/initializer-dist/template/src/app.ts +11 -0
- package/initializer-dist/template/src/capabilities/tool.search-coffee-catalog.ts +103 -0
- package/{dist → initializer-dist}/template/src/server.ts +1 -1
- package/{dist → initializer-dist}/template/test/server.test.mjs +13 -11
- package/{dist → initializer-dist}/template/test-support/llm-server.mjs +1 -1
- package/package.json +29 -13
- package/dist/template/src/app.ts +0 -157
- /package/{dist → initializer-dist}/LICENSE +0 -0
- /package/{dist → initializer-dist}/create.mjs +0 -0
- /package/{dist → initializer-dist}/template/eval/meaning.test.mjs +0 -0
- /package/{dist → initializer-dist}/template/test-support/brewmark-fixture.mjs +0 -0
- /package/{dist → initializer-dist}/template/tsconfig.json +0 -0
package/README.md
CHANGED
|
@@ -1,22 +1,69 @@
|
|
|
1
1
|
# `@emseepea/create-api-backed-server`
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
API. The package builds its starter from the maintained
|
|
5
|
-
[API-backed server example](https://github.com/emseepea/emseepea/tree/main/examples/backend-no-ui).
|
|
6
|
-
|
|
7
|
-
## Create the Project
|
|
8
|
-
|
|
9
|
-
This initializer is queued for the next pre-alpha release and is not yet
|
|
10
|
-
available from npm.
|
|
3
|
+
This directory is both the maintained example and its public npm initializer.
|
|
11
4
|
|
|
12
5
|
```sh
|
|
13
6
|
npm init @emseepea/api-backed-server@next -- my-server
|
|
14
7
|
```
|
|
15
8
|
|
|
9
|
+
<!-- generated-project-readme -->
|
|
10
|
+
|
|
11
|
+
## Public Web Service Backend Example
|
|
12
|
+
|
|
13
|
+
Choose this example when your MCP tool needs to read a public web service and
|
|
14
|
+
return selected, checked data in a clear result for an assistant.
|
|
15
|
+
|
|
16
|
+
This example exposes one read-only `search-coffee-catalog` tool. A normal run
|
|
17
|
+
searches [BrewMark's public coffee catalogue](https://brewmark.io/developers/api-docs)
|
|
18
|
+
and returns at most five coffees.
|
|
19
|
+
|
|
20
|
+
Unlike the [first public tool example](https://github.com/emseepea/emseepea/tree/main/examples/basic-no-ui),
|
|
21
|
+
this tool adapts a separate service. The public input and result use the Model Context Protocol
|
|
22
|
+
(MCP). BrewMark's query and response are checked before the result is returned.
|
|
23
|
+
|
|
24
|
+
The tool preserves BrewMark's useful field names and values. It does not keep a
|
|
25
|
+
second list of roast levels, so BrewMark can add a valid level without requiring
|
|
26
|
+
an MCP release. Separate public and backend schemas still select and check every
|
|
27
|
+
field that can reach the caller.
|
|
28
|
+
|
|
29
|
+
The caller can choose a search term and roast filter. The caller cannot change
|
|
30
|
+
the website, path, result limit, sort order, credentials, or HTTP rules. Search
|
|
31
|
+
terms are sent to BrewMark. The example does not send authentication details or
|
|
32
|
+
change data. Do not include personal, secret, or confidential information in a
|
|
33
|
+
search term.
|
|
34
|
+
|
|
35
|
+
The automated checks use invented coffee records through the same MCP server.
|
|
36
|
+
They do not depend on BrewMark being available and do not make a speed or uptime
|
|
37
|
+
claim for BrewMark. Normal runs use BrewMark's fair-use public web service.
|
|
38
|
+
|
|
39
|
+
## Run
|
|
40
|
+
|
|
41
|
+
From this directory:
|
|
42
|
+
|
|
16
43
|
```sh
|
|
17
|
-
cd my-server
|
|
18
44
|
npm install
|
|
19
|
-
npm
|
|
20
|
-
npm run lint
|
|
45
|
+
npm run build
|
|
21
46
|
npm start
|
|
22
47
|
```
|
|
48
|
+
|
|
49
|
+
The endpoint is `http://127.0.0.1:3000/mcp`.
|
|
50
|
+
|
|
51
|
+
## Check This Example
|
|
52
|
+
|
|
53
|
+
[Ordinary tests](test/) live in `test/`.
|
|
54
|
+
The [AI tool-choice and understanding test](eval/meaning.test.mjs) lives separately in `eval/`.
|
|
55
|
+
The commands below run each suite independently.
|
|
56
|
+
|
|
57
|
+
Run its build, mapping, validation, and MCP checks:
|
|
58
|
+
|
|
59
|
+
```sh
|
|
60
|
+
npm test
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Check that Claude chooses catalogue search and understands the rating scales:
|
|
64
|
+
|
|
65
|
+
```sh
|
|
66
|
+
npm run test:llm
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
If Claude is not already signed in, run `claude auth login` first.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Public Web Service Backend Example
|
|
2
2
|
|
|
3
3
|
Choose this example when your MCP tool needs to read a public web service and
|
|
4
|
-
|
|
4
|
+
return selected, checked data in a clear result for an assistant.
|
|
5
5
|
|
|
6
6
|
This example exposes one read-only `search-coffee-catalog` tool. A normal run
|
|
7
7
|
searches [BrewMark's public coffee catalogue](https://brewmark.io/developers/api-docs)
|
|
@@ -11,6 +11,11 @@ Unlike the [first public tool example](https://github.com/emseepea/emseepea/tree
|
|
|
11
11
|
this tool adapts a separate service. The public input and result use the Model Context Protocol
|
|
12
12
|
(MCP). BrewMark's query and response are checked before the result is returned.
|
|
13
13
|
|
|
14
|
+
The tool preserves BrewMark's useful field names and values. It does not keep a
|
|
15
|
+
second list of roast levels, so BrewMark can add a valid level without requiring
|
|
16
|
+
an MCP release. Separate public and backend schemas still select and check every
|
|
17
|
+
field that can reach the caller.
|
|
18
|
+
|
|
14
19
|
The caller can choose a search term and roast filter. The caller cannot change
|
|
15
20
|
the website, path, result limit, sort order, credentials, or HTTP rules. Search
|
|
16
21
|
terms are sent to BrewMark. The example does not send authentication details or
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "emseepea-starter",
|
|
3
3
|
"version": "0.0.0",
|
|
4
|
-
"
|
|
4
|
+
"description": "Create an Em See Pea server backed by a public web API.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"scripts": {
|
|
@@ -13,10 +13,6 @@
|
|
|
13
13
|
"test:llm:built": "emseepea-test eval",
|
|
14
14
|
"lint": "oxlint src test eval test-support"
|
|
15
15
|
},
|
|
16
|
-
"dependencies": {
|
|
17
|
-
"@emseepea/server": "0.0.4",
|
|
18
|
-
"zod": "4.4.3"
|
|
19
|
-
},
|
|
20
16
|
"devDependencies": {
|
|
21
17
|
"@emseepea/testing": "0.2.1",
|
|
22
18
|
"@modelcontextprotocol/client": "2.0.0",
|
|
@@ -26,5 +22,10 @@
|
|
|
26
22
|
},
|
|
27
23
|
"engines": {
|
|
28
24
|
"node": ">=22"
|
|
25
|
+
},
|
|
26
|
+
"private": true,
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@emseepea/server": "0.2.1",
|
|
29
|
+
"zod": "4.4.3"
|
|
29
30
|
}
|
|
30
31
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { createEmseepea, discoverCapabilities } from "@emseepea/server";
|
|
2
|
+
import type { JsonHttpClient } from "@emseepea/server/http";
|
|
3
|
+
|
|
4
|
+
export async function createBackendExample(client: JsonHttpClient): Promise<ReturnType<typeof createEmseepea>> {
|
|
5
|
+
return createEmseepea({
|
|
6
|
+
name: "emseepea-backend-no-ui",
|
|
7
|
+
version: "0.0.0",
|
|
8
|
+
instructions: "Use search-coffee-catalog to search BrewMark's public coffee catalogue.",
|
|
9
|
+
...await discoverCapabilities(new URL("./capabilities/", import.meta.url), { client }),
|
|
10
|
+
});
|
|
11
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
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 roastLevel = z.string().trim().min(1).max(40);
|
|
8
|
+
const searchInput = z.object({
|
|
9
|
+
query: z.string().trim().min(2).max(80),
|
|
10
|
+
roastLevel: roastLevel.describe("Optional BrewMark roast level, such as LIGHT or MEDIUM_LIGHT.").optional(),
|
|
11
|
+
});
|
|
12
|
+
const coffee = z.object({
|
|
13
|
+
name: z.string().max(200), roasterName: z.string().max(200), roastLevel,
|
|
14
|
+
origin: z.string().max(200).nullable(), processingMethod: z.string().max(100).nullable(),
|
|
15
|
+
flavorProfile: z.string().max(500).nullable(), acidityLevel: z.number().int().min(1).max(5).nullable(),
|
|
16
|
+
bodyLevel: z.number().int().min(1).max(5).nullable(),
|
|
17
|
+
});
|
|
18
|
+
const searchReport = z.object({
|
|
19
|
+
query: z.string().max(80), roastLevelFilter: roastLevel.nullable(), returnedCount: z.number().int().min(0).max(5),
|
|
20
|
+
moreMatchesAvailable: z.boolean(),
|
|
21
|
+
ratingScale: z.object({
|
|
22
|
+
acidity: z.literal("1 = low acidity; 5 = high acidity"),
|
|
23
|
+
body: z.literal("1 = light body; 5 = full body"),
|
|
24
|
+
}),
|
|
25
|
+
coffees: z.array(coffee).max(5), source: z.literal("BrewMark"), sourceUrl: z.literal("https://brewmark.io"),
|
|
26
|
+
});
|
|
27
|
+
const backendCommand = z.object({
|
|
28
|
+
pathname: z.literal("/api/coffees"),
|
|
29
|
+
searchParams: z.object({
|
|
30
|
+
q: z.string().min(2).max(80), roastLevel: roastLevel.optional(),
|
|
31
|
+
sort: z.literal("alpha"), limit: z.literal("5"),
|
|
32
|
+
}),
|
|
33
|
+
});
|
|
34
|
+
const backendCoffee = z.object({
|
|
35
|
+
name: z.string().max(200), roasterName: z.string().max(200), roastLevel,
|
|
36
|
+
origin: z.string().max(200).nullable(), processingMethod: z.string().max(100).nullable(),
|
|
37
|
+
flavorProfile: z.string().max(500).nullable(), acidityLevel: z.number().int().min(1).max(5).nullable(),
|
|
38
|
+
bodyLevel: z.number().int().min(1).max(5).nullable(),
|
|
39
|
+
});
|
|
40
|
+
const backendPayload = z.object({
|
|
41
|
+
data: z.array(backendCoffee).max(5), cursor: z.string().max(2_048).nullable(), hasMore: z.boolean(),
|
|
42
|
+
});
|
|
43
|
+
const backendResult = z.object({ request: backendCommand, payload: backendPayload });
|
|
44
|
+
|
|
45
|
+
export default (({ client }) => defineMappedTool({
|
|
46
|
+
name: "search-coffee-catalog",
|
|
47
|
+
access: "public",
|
|
48
|
+
description: "Search BrewMark's public coffee catalogue and explain its acidity and body ratings.",
|
|
49
|
+
inputSchema: searchInput,
|
|
50
|
+
outputSchema: searchReport,
|
|
51
|
+
backendInputSchema: backendCommand,
|
|
52
|
+
backendOutputSchema: backendResult,
|
|
53
|
+
mapInput: ({ query, roastLevel }) => ({
|
|
54
|
+
pathname: "/api/coffees" as const,
|
|
55
|
+
searchParams: {
|
|
56
|
+
q: query,
|
|
57
|
+
...(roastLevel ? { roastLevel } : {}),
|
|
58
|
+
sort: "alpha" as const,
|
|
59
|
+
limit: "5" as const,
|
|
60
|
+
},
|
|
61
|
+
}),
|
|
62
|
+
async adapter(request, { signal, deadlineMs }) {
|
|
63
|
+
return { request, payload: await client.get({ ...request, signal, deadlineMs }) };
|
|
64
|
+
},
|
|
65
|
+
mapOutput: ({ request, payload }) => {
|
|
66
|
+
const coffees = payload.data.map((record) => ({
|
|
67
|
+
name: record.name, roasterName: record.roasterName, roastLevel: record.roastLevel,
|
|
68
|
+
origin: record.origin, processingMethod: record.processingMethod,
|
|
69
|
+
flavorProfile: record.flavorProfile, acidityLevel: record.acidityLevel, bodyLevel: record.bodyLevel,
|
|
70
|
+
}));
|
|
71
|
+
const data = {
|
|
72
|
+
query: request.searchParams.q,
|
|
73
|
+
roastLevelFilter: request.searchParams.roastLevel ?? null,
|
|
74
|
+
returnedCount: coffees.length,
|
|
75
|
+
moreMatchesAvailable: payload.hasMore,
|
|
76
|
+
ratingScale: {
|
|
77
|
+
acidity: "1 = low acidity; 5 = high acidity" as const,
|
|
78
|
+
body: "1 = light body; 5 = full body" as const,
|
|
79
|
+
},
|
|
80
|
+
coffees,
|
|
81
|
+
source: "BrewMark" as const,
|
|
82
|
+
sourceUrl: "https://brewmark.io" as const,
|
|
83
|
+
};
|
|
84
|
+
const lines = coffees.map((record) => [
|
|
85
|
+
`${record.name} by ${record.roasterName}`,
|
|
86
|
+
`origin: ${record.origin ?? "not provided"}`,
|
|
87
|
+
`roast level: ${record.roastLevel}`,
|
|
88
|
+
`acidity: ${record.acidityLevel ?? "not provided"}`,
|
|
89
|
+
`body: ${record.bodyLevel ?? "not provided"}`,
|
|
90
|
+
].join("; "));
|
|
91
|
+
return {
|
|
92
|
+
text: [
|
|
93
|
+
`BrewMark returned ${coffees.length} coffee${coffees.length === 1 ? "" : "s"} for “${data.query}”.`,
|
|
94
|
+
`More matches available: ${data.moreMatchesAvailable ? "yes" : "no"}.`,
|
|
95
|
+
"Acidity: 1 = low acidity; 5 = high acidity.",
|
|
96
|
+
"Body: 1 = light body; 5 = full body.",
|
|
97
|
+
...lines,
|
|
98
|
+
"Source: https://brewmark.io",
|
|
99
|
+
].join("\n"),
|
|
100
|
+
data,
|
|
101
|
+
};
|
|
102
|
+
},
|
|
103
|
+
})) satisfies CapabilityModuleFactory<BackendExampleContext>;
|
|
@@ -5,10 +5,10 @@ import { serveEmseepea } from "@emseepea/server";
|
|
|
5
5
|
import { createBackendExample } from "../dist/app.js";
|
|
6
6
|
import { brewmarkFixture } from "../test-support/brewmark-fixture.mjs";
|
|
7
7
|
|
|
8
|
-
test("the backend example
|
|
8
|
+
test("the backend example checks and passes through selected BrewMark values", 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;
|
|
@@ -38,12 +38,12 @@ test("the backend example maps, checks, and explains BrewMark data", async () =>
|
|
|
38
38
|
|
|
39
39
|
const result = await client.callTool({
|
|
40
40
|
name: "search-coffee-catalog",
|
|
41
|
-
arguments: { query: " natural ",
|
|
41
|
+
arguments: { query: " natural ", roastLevel: "LIGHT" },
|
|
42
42
|
});
|
|
43
43
|
assert.equal(result.isError, false);
|
|
44
44
|
assert.deepEqual(result.structuredContent, {
|
|
45
45
|
query: "natural",
|
|
46
|
-
|
|
46
|
+
roastLevelFilter: "LIGHT",
|
|
47
47
|
returnedCount: 1,
|
|
48
48
|
moreMatchesAvailable: true,
|
|
49
49
|
ratingScale: {
|
|
@@ -53,11 +53,11 @@ test("the backend example maps, checks, and explains BrewMark data", async () =>
|
|
|
53
53
|
coffees: [
|
|
54
54
|
{
|
|
55
55
|
name: "Riverlight Natural",
|
|
56
|
-
|
|
56
|
+
roasterName: "North Star Sample Roasters",
|
|
57
|
+
roastLevel: "LIGHT",
|
|
57
58
|
origin: "Burundi",
|
|
58
|
-
roast: "light",
|
|
59
59
|
processingMethod: "Natural",
|
|
60
|
-
|
|
60
|
+
flavorProfile: "Blackberry, hibiscus",
|
|
61
61
|
acidityLevel: 5,
|
|
62
62
|
bodyLevel: 2,
|
|
63
63
|
},
|
|
@@ -81,12 +81,13 @@ test("the backend example maps, checks, and explains BrewMark data", async () =>
|
|
|
81
81
|
data: [{
|
|
82
82
|
name: "Details Pending",
|
|
83
83
|
roasterName: "Sample Coffee",
|
|
84
|
-
roastLevel: "
|
|
84
|
+
roastLevel: "OMNI_ROAST",
|
|
85
85
|
origin: null,
|
|
86
86
|
processingMethod: null,
|
|
87
87
|
flavorProfile: null,
|
|
88
88
|
acidityLevel: null,
|
|
89
89
|
bodyLevel: null,
|
|
90
|
+
privateNote: "do not expose",
|
|
90
91
|
}],
|
|
91
92
|
cursor: null,
|
|
92
93
|
hasMore: false,
|
|
@@ -98,14 +99,15 @@ test("the backend example maps, checks, and explains BrewMark data", async () =>
|
|
|
98
99
|
assert.equal(missingDetails.isError, false);
|
|
99
100
|
assert.deepEqual(missingDetails.structuredContent.coffees[0], {
|
|
100
101
|
name: "Details Pending",
|
|
101
|
-
|
|
102
|
+
roasterName: "Sample Coffee",
|
|
103
|
+
roastLevel: "OMNI_ROAST",
|
|
102
104
|
origin: null,
|
|
103
|
-
roast: "light",
|
|
104
105
|
processingMethod: null,
|
|
105
|
-
|
|
106
|
+
flavorProfile: null,
|
|
106
107
|
acidityLevel: null,
|
|
107
108
|
bodyLevel: null,
|
|
108
109
|
});
|
|
110
|
+
assert.equal("privateNote" in missingDetails.structuredContent.coffees[0], false);
|
|
109
111
|
assert.match(missingDetails.content[0].text, /origin: not provided/);
|
|
110
112
|
assert.match(missingDetails.content[0].text, /acidity: not provided/);
|
|
111
113
|
assert.match(missingDetails.content[0].text, /body: not provided/);
|
|
@@ -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,22 +1,38 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@emseepea/create-api-backed-server",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.3",
|
|
4
4
|
"description": "Create an Em See Pea server backed by a public web API.",
|
|
5
5
|
"license": "MIT",
|
|
6
|
-
"
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
"
|
|
6
|
+
"type": "module",
|
|
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
|
+
"start": "node dist/server.js",
|
|
13
|
+
"test": "npm run build && npm run test:built",
|
|
14
|
+
"test:built": "node --test test/*.test.mjs",
|
|
15
|
+
"test:llm": "npm run build && npm run test:llm:built",
|
|
16
|
+
"test:llm:built": "emseepea-test eval",
|
|
17
|
+
"lint": "oxlint src test eval test-support",
|
|
18
|
+
"prepack": "npm run build:initializer"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@emseepea/server": "0.2.1",
|
|
22
|
+
"zod": "4.4.3",
|
|
23
|
+
"@emseepea/testing": "0.2.1",
|
|
24
|
+
"@modelcontextprotocol/client": "2.0.0",
|
|
25
|
+
"@types/node": "24.13.3",
|
|
26
|
+
"typescript": "6.0.3",
|
|
27
|
+
"oxlint": "1.80.0"
|
|
28
|
+
},
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=22"
|
|
10
31
|
},
|
|
32
|
+
"repository": { "type": "git", "url": "git+https://github.com/emseepea/emseepea.git", "directory": "examples/backend-no-ui" },
|
|
11
33
|
"homepage": "https://emseepea.github.io/emseepea/examples/",
|
|
12
34
|
"bugs": "https://github.com/emseepea/emseepea/issues",
|
|
13
35
|
"publishConfig": { "access": "public", "provenance": true, "tag": "next" },
|
|
14
|
-
"
|
|
15
|
-
"
|
|
16
|
-
"files": ["dist"],
|
|
17
|
-
"scripts": {
|
|
18
|
-
"build": "node ../../scripts/build-initializer.mjs",
|
|
19
|
-
"prepack": "npm run build"
|
|
20
|
-
},
|
|
21
|
-
"engines": { "node": ">=22" }
|
|
36
|
+
"bin": { "create-api-backed-server": "./initializer-dist/create.mjs" },
|
|
37
|
+
"files": ["initializer-dist"]
|
|
22
38
|
}
|
package/dist/template/src/app.ts
DELETED
|
@@ -1,157 +0,0 @@
|
|
|
1
|
-
import { createEmseepea, defineMappedTool } from "@emseepea/server";
|
|
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
|
-
|
|
151
|
-
return createEmseepea({
|
|
152
|
-
name: "emseepea-backend-no-ui",
|
|
153
|
-
version: "0.0.0",
|
|
154
|
-
instructions: "Use search-coffee-catalog to search BrewMark's public coffee catalogue.",
|
|
155
|
-
tools: [searchCoffeeCatalog],
|
|
156
|
-
});
|
|
157
|
-
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|