@emseepea/create-mongodb-backed-server 0.0.1 → 0.0.5
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 +31 -0
- package/initializer-dist/template/README.md +32 -1
- package/initializer-dist/template/eval/meaning.test.mjs +17 -7
- package/initializer-dist/template/package.json +3 -2
- package/initializer-dist/template/src/app.ts +17 -3
- package/initializer-dist/template/src/capabilities/tool.add-pea-variety.ts +3 -3
- package/initializer-dist/template/src/capabilities/tool.list-pea-observations.ts +3 -3
- package/initializer-dist/template/src/capabilities/tool.list-pea-varieties.ts +3 -3
- package/initializer-dist/template/src/capabilities/tool.record-pea-observation.ts +3 -3
- package/initializer-dist/template/src/database.ts +2 -0
- package/initializer-dist/template/test/server.test.mjs +21 -1
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -27,6 +27,30 @@ tests.
|
|
|
27
27
|
|
|
28
28
|
<!-- generated-project-readme -->
|
|
29
29
|
|
|
30
|
+
## Choose Open or Protected Access
|
|
31
|
+
|
|
32
|
+
Start open when the catalogue and operations are public.
|
|
33
|
+
|
|
34
|
+
To protect this template, pass both options to the app factory:
|
|
35
|
+
|
|
36
|
+
- `access: { access: "protected", requiredScopes: ["peas:read"] }`
|
|
37
|
+
- an `authentication` adapter
|
|
38
|
+
|
|
39
|
+
Keep `authentication.discovery` as `"public"` unless capability names or
|
|
40
|
+
schemas are sensitive. Use `"protected"` only when each principal should see a
|
|
41
|
+
permission-filtered catalogue. OAuth metadata remains public in both modes.
|
|
42
|
+
|
|
43
|
+
## Add Observability
|
|
44
|
+
|
|
45
|
+
The same factory accepts `observability`.
|
|
46
|
+
|
|
47
|
+
- Use `structuredLogging` for safe structured events.
|
|
48
|
+
- Use `openTelemetry` for traces and metrics.
|
|
49
|
+
|
|
50
|
+
Adapters receive only redacted framework events. They never receive request
|
|
51
|
+
bodies, arguments, results, tokens, provider claims, or raw errors. See the
|
|
52
|
+
[server API](https://github.com/emseepea/emseepea/tree/main/packages/framework#authentication-and-observability) for the complete configuration.
|
|
53
|
+
|
|
30
54
|
## MongoDB-Backed Server
|
|
31
55
|
|
|
32
56
|
`pea_varieties` is schema-enforced. `src/pea-document.ts` contains its only
|
|
@@ -90,6 +114,13 @@ The tools never accept database operators, collection names, sort documents,
|
|
|
90
114
|
or destinations. The pool and database operations are bounded, and provider
|
|
91
115
|
failures return a generic error.
|
|
92
116
|
|
|
117
|
+
## Add Feedback
|
|
118
|
+
|
|
119
|
+
Install `@emseepea/feedback` when this server needs a detailed one-way
|
|
120
|
+
observation or a durable support conversation. Pass its tools through the
|
|
121
|
+
application factory's `additionalTools` option. Choose PostgreSQL, Firestore,
|
|
122
|
+
GitHub Issues, or Zendesk in the [feedback guide](../../packages/feedback/README.md).
|
|
123
|
+
|
|
93
124
|
## Check This Project
|
|
94
125
|
|
|
95
126
|
Run lint, build, and ordinary database integration tests without spending model
|
|
@@ -1,4 +1,28 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Choose Open or Protected Access
|
|
2
|
+
|
|
3
|
+
Start open when the catalogue and operations are public.
|
|
4
|
+
|
|
5
|
+
To protect this template, pass both options to the app factory:
|
|
6
|
+
|
|
7
|
+
- `access: { access: "protected", requiredScopes: ["peas:read"] }`
|
|
8
|
+
- an `authentication` adapter
|
|
9
|
+
|
|
10
|
+
Keep `authentication.discovery` as `"public"` unless capability names or
|
|
11
|
+
schemas are sensitive. Use `"protected"` only when each principal should see a
|
|
12
|
+
permission-filtered catalogue. OAuth metadata remains public in both modes.
|
|
13
|
+
|
|
14
|
+
## Add Observability
|
|
15
|
+
|
|
16
|
+
The same factory accepts `observability`.
|
|
17
|
+
|
|
18
|
+
- Use `structuredLogging` for safe structured events.
|
|
19
|
+
- Use `openTelemetry` for traces and metrics.
|
|
20
|
+
|
|
21
|
+
Adapters receive only redacted framework events. They never receive request
|
|
22
|
+
bodies, arguments, results, tokens, provider claims, or raw errors. See the
|
|
23
|
+
[server API](https://github.com/emseepea/emseepea/tree/main/packages/framework#authentication-and-observability) for the complete configuration.
|
|
24
|
+
|
|
25
|
+
## MongoDB-Backed Server
|
|
2
26
|
|
|
3
27
|
`pea_varieties` is schema-enforced. `src/pea-document.ts` contains its only
|
|
4
28
|
stored-document schema. MongoDB applies that exact object as its collection
|
|
@@ -61,6 +85,13 @@ The tools never accept database operators, collection names, sort documents,
|
|
|
61
85
|
or destinations. The pool and database operations are bounded, and provider
|
|
62
86
|
failures return a generic error.
|
|
63
87
|
|
|
88
|
+
## Add Feedback
|
|
89
|
+
|
|
90
|
+
Install `@emseepea/feedback` when this server needs a detailed one-way
|
|
91
|
+
observation or a durable support conversation. Pass its tools through the
|
|
92
|
+
application factory's `additionalTools` option. Choose PostgreSQL, Firestore,
|
|
93
|
+
GitHub Issues, or Zendesk in the [feedback guide](../../packages/feedback/README.md).
|
|
94
|
+
|
|
64
95
|
## Check This Project
|
|
65
96
|
|
|
66
97
|
Run lint, build, and ordinary database integration tests without spending model
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import test from "node:test";
|
|
2
2
|
import {
|
|
3
|
+
assertNoNegativeFeedback,
|
|
3
4
|
assertResponseContains,
|
|
4
5
|
assertResponseMeaning,
|
|
5
|
-
|
|
6
|
+
assertToolCallsWithOptionalFeedback,
|
|
6
7
|
createConversation,
|
|
7
8
|
} from "@emseepea/testing/semantic";
|
|
8
9
|
|
|
10
|
+
const server = new URL(import.meta.resolve("@emseepea/feedback/testing-server"));
|
|
11
|
+
const appModule = new URL("../dist/app.js", import.meta.url).href;
|
|
12
|
+
|
|
9
13
|
const trialUris = [1, 2, 3].map((trial) => {
|
|
10
14
|
const value = process.env[`MONGODB_URL_TRIAL_${trial}`];
|
|
11
15
|
if (!value) throw new Error(`MONGODB_URL_TRIAL_${trial} is required`);
|
|
@@ -14,14 +18,19 @@ const trialUris = [1, 2, 3].map((trial) => {
|
|
|
14
18
|
|
|
15
19
|
test("finds and adds MongoDB-backed pea varieties through natural requests", async (t) => {
|
|
16
20
|
const chat = await createConversation(t, {
|
|
17
|
-
server
|
|
18
|
-
environment: (trial) => ({
|
|
21
|
+
server,
|
|
22
|
+
environment: (trial) => ({
|
|
23
|
+
MONGODB_URL: trialUris[trial - 1],
|
|
24
|
+
EMSEEPEA_EVAL_APP_MODULE: appModule,
|
|
25
|
+
EMSEEPEA_EVAL_APP_FACTORY: "createMongoExample",
|
|
26
|
+
EMSEEPEA_EVAL_APP_KIND: "mongodb",
|
|
27
|
+
}),
|
|
19
28
|
});
|
|
20
29
|
|
|
21
30
|
// The read and write turns cover both public decisions. Storage validation
|
|
22
31
|
// remains in deterministic tests because asking a model cannot prove it.
|
|
23
32
|
const fastest = await chat.send("Among the saved snap pea varieties, which matures fastest?");
|
|
24
|
-
|
|
33
|
+
await assertToolCallsWithOptionalFeedback(fastest, [{
|
|
25
34
|
name: "list-pea-varieties",
|
|
26
35
|
arguments: { pea_type: "snap" },
|
|
27
36
|
}]);
|
|
@@ -33,7 +42,7 @@ test("finds and adds MongoDB-backed pea varieties through natural requests", asy
|
|
|
33
42
|
"Add Golden Sweet as a climbing mangetout pea that matures in 70 days. " +
|
|
34
43
|
"Use exactly mangetout as its pea type. Its notes are: Purple flowers and flat edible pods.",
|
|
35
44
|
);
|
|
36
|
-
|
|
45
|
+
await assertToolCallsWithOptionalFeedback(added, [{
|
|
37
46
|
name: "add-pea-variety",
|
|
38
47
|
arguments: {
|
|
39
48
|
name: "Golden Sweet",
|
|
@@ -51,7 +60,7 @@ test("finds and adds MongoDB-backed pea varieties through natural requests", asy
|
|
|
51
60
|
"Record that Golden Sweet was flowering in the west trellis on 2026-09-08. " +
|
|
52
61
|
"The notes are: First flower opened.",
|
|
53
62
|
);
|
|
54
|
-
|
|
63
|
+
await assertToolCallsWithOptionalFeedback(recorded, [{
|
|
55
64
|
name: "record-pea-observation",
|
|
56
65
|
arguments: {
|
|
57
66
|
variety_name: "Golden Sweet",
|
|
@@ -63,7 +72,7 @@ test("finds and adds MongoDB-backed pea varieties through natural requests", asy
|
|
|
63
72
|
}]);
|
|
64
73
|
|
|
65
74
|
const observations = await chat.send("What have I observed about Golden Sweet?");
|
|
66
|
-
|
|
75
|
+
await assertToolCallsWithOptionalFeedback(observations, [{
|
|
67
76
|
name: "list-pea-observations",
|
|
68
77
|
arguments: { variety_name: "Golden Sweet" },
|
|
69
78
|
}]);
|
|
@@ -72,4 +81,5 @@ test("finds and adds MongoDB-backed pea varieties through natural requests", asy
|
|
|
72
81
|
"Golden Sweet was flowering in the west trellis on 8 September 2026, " +
|
|
73
82
|
"and the first flower had opened.",
|
|
74
83
|
});
|
|
84
|
+
assertNoNegativeFeedback(fastest, added, recorded, observations);
|
|
75
85
|
});
|
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
"lint": "oxlint src test eval"
|
|
17
17
|
},
|
|
18
18
|
"devDependencies": {
|
|
19
|
-
"@emseepea/
|
|
19
|
+
"@emseepea/feedback": "0.2.0",
|
|
20
|
+
"@emseepea/testing": "0.9.3",
|
|
20
21
|
"@types/node": "24.13.3",
|
|
21
22
|
"typescript": "6.0.3",
|
|
22
23
|
"oxlint": "1.80.0"
|
|
@@ -26,7 +27,7 @@
|
|
|
26
27
|
},
|
|
27
28
|
"private": true,
|
|
28
29
|
"dependencies": {
|
|
29
|
-
"@emseepea/server": "0.
|
|
30
|
+
"@emseepea/server": "0.6.1",
|
|
30
31
|
"ajv": "8.20.0",
|
|
31
32
|
"json-schema-to-ts": "3.1.1",
|
|
32
33
|
"mongodb": "7.6.0",
|
|
@@ -1,13 +1,25 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
createEmseepea,
|
|
3
|
+
discoverCapabilities,
|
|
4
|
+
type AccessPolicy,
|
|
5
|
+
type EmseepeaExtensions,
|
|
6
|
+
} from "@emseepea/server";
|
|
2
7
|
import type { Collection, Db } from "mongodb";
|
|
3
8
|
import { z } from "zod";
|
|
4
9
|
import { createDatabase } from "./database.js";
|
|
5
10
|
import type { PeaObservationDocument } from "./pea-observation-document.js";
|
|
6
11
|
import type { PeaDocument } from "./pea-document.js";
|
|
7
12
|
|
|
8
|
-
export interface MongoExampleOptions
|
|
13
|
+
export interface MongoExampleOptions extends EmseepeaExtensions {
|
|
14
|
+
readonly uri: string;
|
|
15
|
+
readonly access?: AccessPolicy;
|
|
16
|
+
}
|
|
9
17
|
|
|
10
|
-
export async function createMongoExample({
|
|
18
|
+
export async function createMongoExample({
|
|
19
|
+
uri,
|
|
20
|
+
access = { access: "public" },
|
|
21
|
+
...extensions
|
|
22
|
+
}: MongoExampleOptions) {
|
|
11
23
|
const parsedUri = z.string().url().refine(
|
|
12
24
|
(value) => ["mongodb:", "mongodb+srv:"].includes(new URL(value).protocol),
|
|
13
25
|
"uri must use MongoDB",
|
|
@@ -33,10 +45,12 @@ export async function createMongoExample({ uri }: MongoExampleOptions) {
|
|
|
33
45
|
},
|
|
34
46
|
readinessTimeoutMs: 2_500,
|
|
35
47
|
...await discoverCapabilities(new URL("./capabilities/", import.meta.url), {
|
|
48
|
+
access,
|
|
36
49
|
database: () => database,
|
|
37
50
|
observations: () => observations,
|
|
38
51
|
varieties: () => varieties,
|
|
39
52
|
}),
|
|
53
|
+
...extensions,
|
|
40
54
|
});
|
|
41
55
|
const closeProvider = async () => {
|
|
42
56
|
observations = undefined;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import { defineTool, type CapabilityModuleFactory } from "@emseepea/server";
|
|
2
|
+
import { defineTool, type CapabilityModuleFactory, type ToolContext } from "@emseepea/server";
|
|
3
3
|
import { parsePeaDocument } from "../pea-document.js";
|
|
4
4
|
import { varietySchema } from "../pea-variety.js";
|
|
5
5
|
import type { MongoContext } from "../database.js";
|
|
@@ -9,11 +9,11 @@ const outputSchema = varietySchema;
|
|
|
9
9
|
|
|
10
10
|
export default ((context) => defineTool({
|
|
11
11
|
name: "add-pea-variety",
|
|
12
|
-
access
|
|
12
|
+
...context.access,
|
|
13
13
|
description: "Add one pea variety to the catalogue.",
|
|
14
14
|
inputSchema,
|
|
15
15
|
outputSchema,
|
|
16
|
-
async handler(variety, { signal }) {
|
|
16
|
+
async handler(variety, { signal }: ToolContext) {
|
|
17
17
|
const collection = context.varieties();
|
|
18
18
|
if (!collection) throw new Error("Variety provider unavailable");
|
|
19
19
|
signal.throwIfAborted();
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { defineTool, type CapabilityModuleFactory } from "@emseepea/server";
|
|
1
|
+
import { defineTool, type CapabilityModuleFactory, type ToolContext } from "@emseepea/server";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import type { MongoContext } from "../database.js";
|
|
4
4
|
import { parsePeaObservationDocument } from "../pea-observation-document.js";
|
|
@@ -15,11 +15,11 @@ const outputSchema = z.object({
|
|
|
15
15
|
|
|
16
16
|
export default ((context) => defineTool({
|
|
17
17
|
name: "list-pea-observations",
|
|
18
|
-
access
|
|
18
|
+
...context.access,
|
|
19
19
|
description: "List up to 20 recent observations of pea plants.",
|
|
20
20
|
inputSchema,
|
|
21
21
|
outputSchema,
|
|
22
|
-
async handler({ variety_name }, { signal }) {
|
|
22
|
+
async handler({ variety_name }, { signal }: ToolContext) {
|
|
23
23
|
const collection = context.observations();
|
|
24
24
|
if (!collection) throw new Error("Observation provider unavailable");
|
|
25
25
|
signal.throwIfAborted();
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { defineTool, type CapabilityModuleFactory } from "@emseepea/server";
|
|
1
|
+
import { defineTool, type CapabilityModuleFactory, type ToolContext } from "@emseepea/server";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { parsePeaDocument } from "../pea-document.js";
|
|
4
4
|
import { varietySchema } from "../pea-variety.js";
|
|
@@ -14,11 +14,11 @@ const outputSchema = z.object({
|
|
|
14
14
|
|
|
15
15
|
export default ((context) => defineTool({
|
|
16
16
|
name: "list-pea-varieties",
|
|
17
|
-
access
|
|
17
|
+
...context.access,
|
|
18
18
|
description: "List up to 20 pea varieties, optionally filtered by pea type.",
|
|
19
19
|
inputSchema,
|
|
20
20
|
outputSchema,
|
|
21
|
-
async handler({ pea_type }, { signal }) {
|
|
21
|
+
async handler({ pea_type }, { signal }: ToolContext) {
|
|
22
22
|
const collection = context.varieties();
|
|
23
23
|
if (!collection) throw new Error("Variety provider unavailable");
|
|
24
24
|
signal.throwIfAborted();
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import { defineTool, type CapabilityModuleFactory } from "@emseepea/server";
|
|
2
|
+
import { defineTool, type CapabilityModuleFactory, type ToolContext } from "@emseepea/server";
|
|
3
3
|
import type { MongoContext } from "../database.js";
|
|
4
4
|
import { parsePeaObservationDocument } from "../pea-observation-document.js";
|
|
5
5
|
import { observationSchema } from "../pea-observation.js";
|
|
@@ -9,11 +9,11 @@ const outputSchema = observationSchema;
|
|
|
9
9
|
|
|
10
10
|
export default ((context) => defineTool({
|
|
11
11
|
name: "record-pea-observation",
|
|
12
|
-
access
|
|
12
|
+
...context.access,
|
|
13
13
|
description: "Record one dated observation of a pea plant.",
|
|
14
14
|
inputSchema,
|
|
15
15
|
outputSchema,
|
|
16
|
-
async handler(observation, { signal }) {
|
|
16
|
+
async handler(observation, { signal }: ToolContext) {
|
|
17
17
|
const collection = context.observations();
|
|
18
18
|
if (!collection) throw new Error("Observation provider unavailable");
|
|
19
19
|
signal.throwIfAborted();
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { MongoClient, type Collection, type Db } from "mongodb";
|
|
2
|
+
import type { AccessPolicy } from "@emseepea/server";
|
|
2
3
|
import type { PeaObservationDocument } from "./pea-observation-document.js";
|
|
3
4
|
import type { PeaDocument } from "./pea-document.js";
|
|
4
5
|
|
|
@@ -23,6 +24,7 @@ export function createDatabase(uri: string) {
|
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
export interface MongoContext {
|
|
27
|
+
readonly access: AccessPolicy;
|
|
26
28
|
readonly varieties: () => Collection<PeaDocument> | undefined;
|
|
27
29
|
readonly observations: () => Collection<PeaObservationDocument> | undefined;
|
|
28
30
|
readonly database: () => Db | undefined;
|
|
@@ -2,7 +2,11 @@ import assert from "node:assert/strict";
|
|
|
2
2
|
import { spawnSync } from "node:child_process";
|
|
3
3
|
import test, { after } from "node:test";
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
insecureTestAuthentication,
|
|
7
|
+
startEmseepea,
|
|
8
|
+
startMcpServer,
|
|
9
|
+
} from "@emseepea/testing";
|
|
6
10
|
import { MongoClient } from "mongodb";
|
|
7
11
|
import { createMongoExample } from "../dist/app.js";
|
|
8
12
|
import {
|
|
@@ -57,6 +61,22 @@ test("each collection has one application schema and only varieties enforce it i
|
|
|
57
61
|
]);
|
|
58
62
|
});
|
|
59
63
|
|
|
64
|
+
test("the same template composes protected access and observability", async (t) => {
|
|
65
|
+
const events = [];
|
|
66
|
+
const permissions = ["catalogue:write"];
|
|
67
|
+
const { app } = await createMongoExample({
|
|
68
|
+
uri,
|
|
69
|
+
access: { access: "protected", requiredScopes: permissions },
|
|
70
|
+
authentication: insecureTestAuthentication(permissions),
|
|
71
|
+
observability: [{ id: "test-log", emit: (event) => events.push(event) }],
|
|
72
|
+
});
|
|
73
|
+
const running = await startEmseepea(t, app);
|
|
74
|
+
const client = await running.connect("test-token");
|
|
75
|
+
const result = await client.callTool({ name: "list-pea-varieties", arguments: {} });
|
|
76
|
+
assert.equal(result.isError, false);
|
|
77
|
+
assert.ok(events.some(({ capability }) => capability === "list-pea-varieties"));
|
|
78
|
+
});
|
|
79
|
+
|
|
60
80
|
test("setup fails closed when the schemaless collection already has a validator", async () => {
|
|
61
81
|
await database.command({
|
|
62
82
|
collMod: observationCollectionName,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@emseepea/create-mongodb-backed-server",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.5",
|
|
4
4
|
"description": "Create an Em See Pea server with schema-enforced and schemaless MongoDB collections.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -20,12 +20,13 @@
|
|
|
20
20
|
"prepack": "npm run build:initializer"
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|
|
23
|
-
"@emseepea/
|
|
23
|
+
"@emseepea/feedback": "0.2.0",
|
|
24
|
+
"@emseepea/server": "0.6.1",
|
|
24
25
|
"ajv": "8.20.0",
|
|
25
26
|
"json-schema-to-ts": "3.1.1",
|
|
26
27
|
"mongodb": "7.6.0",
|
|
27
28
|
"zod": "4.4.3",
|
|
28
|
-
"@emseepea/testing": "0.
|
|
29
|
+
"@emseepea/testing": "0.9.3",
|
|
29
30
|
"@types/node": "24.13.3",
|
|
30
31
|
"typescript": "6.0.3",
|
|
31
32
|
"oxlint": "1.80.0"
|