@emseepea/create-database-schema-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/compose.yaml +1 -1
- package/initializer-dist/template/eval/meaning.test.mjs +15 -5
- package/initializer-dist/template/package.json +3 -2
- package/initializer-dist/template/src/app.ts +12 -3
- package/initializer-dist/template/src/capabilities/context.ts +2 -0
- package/initializer-dist/template/src/capabilities/tool.add-pea-variety.ts +3 -3
- package/initializer-dist/template/src/capabilities/tool.list-pea-varieties.ts +3 -3
- package/initializer-dist/template/src/capabilities/tool.summarize-pea-catalog.ts +3 -3
- package/initializer-dist/template/test/server.test.mjs +22 -1
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -28,6 +28,30 @@ Compose setup, lint checks, ordinary tests, and semantic tests.
|
|
|
28
28
|
|
|
29
29
|
<!-- generated-project-readme -->
|
|
30
30
|
|
|
31
|
+
## Choose Open or Protected Access
|
|
32
|
+
|
|
33
|
+
Start open when the catalogue and operations are public.
|
|
34
|
+
|
|
35
|
+
To protect this template, pass both options to the app factory:
|
|
36
|
+
|
|
37
|
+
- `access: { access: "protected", requiredScopes: ["peas:read"] }`
|
|
38
|
+
- an `authentication` adapter
|
|
39
|
+
|
|
40
|
+
Keep `authentication.discovery` as `"public"` unless capability names or
|
|
41
|
+
schemas are sensitive. Use `"protected"` only when each principal should see a
|
|
42
|
+
permission-filtered catalogue. OAuth metadata remains public in both modes.
|
|
43
|
+
|
|
44
|
+
## Add Observability
|
|
45
|
+
|
|
46
|
+
The same factory accepts `observability`.
|
|
47
|
+
|
|
48
|
+
- Use `structuredLogging` for safe structured events.
|
|
49
|
+
- Use `openTelemetry` for traces and metrics.
|
|
50
|
+
|
|
51
|
+
Adapters receive only redacted framework events. They never receive request
|
|
52
|
+
bodies, arguments, results, tokens, provider claims, or raw errors. See the
|
|
53
|
+
[server API](https://github.com/emseepea/emseepea/tree/main/packages/framework#authentication-and-observability) for the complete configuration.
|
|
54
|
+
|
|
31
55
|
## Database Schema Server
|
|
32
56
|
|
|
33
57
|
PostgreSQL is authoritative for the internal contract. Kanel and Kanel Zod
|
|
@@ -85,6 +109,13 @@ The tools never accept SQL, operators, table names, sort expressions, or
|
|
|
85
109
|
destinations from the caller. PostgreSQL statements have a 1.5 second timeout,
|
|
86
110
|
the connection pool is bounded, and provider failures return a generic error.
|
|
87
111
|
|
|
112
|
+
## Add Feedback
|
|
113
|
+
|
|
114
|
+
Install `@emseepea/feedback` when this server needs a detailed one-way
|
|
115
|
+
observation or a durable support conversation. Pass its tools through the
|
|
116
|
+
application factory's `additionalTools` option. Choose PostgreSQL, Firestore,
|
|
117
|
+
GitHub Issues, or Zendesk in the [feedback guide](../../packages/feedback/README.md).
|
|
118
|
+
|
|
88
119
|
## Check This Project
|
|
89
120
|
|
|
90
121
|
Run generation, lint, build, and ordinary integration tests without spending
|
|
@@ -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
|
+
## Database Schema Server
|
|
2
26
|
|
|
3
27
|
PostgreSQL is authoritative for the internal contract. Kanel and Kanel Zod
|
|
4
28
|
generate TypeScript and runtime validators from the committed schema. Normal
|
|
@@ -55,6 +79,13 @@ The tools never accept SQL, operators, table names, sort expressions, or
|
|
|
55
79
|
destinations from the caller. PostgreSQL statements have a 1.5 second timeout,
|
|
56
80
|
the connection pool is bounded, and provider failures return a generic error.
|
|
57
81
|
|
|
82
|
+
## Add Feedback
|
|
83
|
+
|
|
84
|
+
Install `@emseepea/feedback` when this server needs a detailed one-way
|
|
85
|
+
observation or a durable support conversation. Pass its tools through the
|
|
86
|
+
application factory's `additionalTools` option. Choose PostgreSQL, Firestore,
|
|
87
|
+
GitHub Issues, or Zendesk in the [feedback guide](../../packages/feedback/README.md).
|
|
88
|
+
|
|
58
89
|
## Check This Project
|
|
59
90
|
|
|
60
91
|
Run generation, lint, build, and ordinary integration tests without spending
|
|
@@ -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 trialDatabaseUrls = [1, 2, 3].map((trial) => {
|
|
10
14
|
const value = process.env[`DATABASE_URL_TRIAL_${trial}`];
|
|
11
15
|
if (!value) throw new Error(`DATABASE_URL_TRIAL_${trial} is required`);
|
|
@@ -14,15 +18,20 @@ const trialDatabaseUrls = [1, 2, 3].map((trial) => {
|
|
|
14
18
|
|
|
15
19
|
test("finds and adds 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
|
+
DATABASE_URL: trialDatabaseUrls[trial - 1],
|
|
24
|
+
EMSEEPEA_EVAL_APP_MODULE: appModule,
|
|
25
|
+
EMSEEPEA_EVAL_APP_FACTORY: "createDatabaseSchemaExample",
|
|
26
|
+
EMSEEPEA_EVAL_APP_KIND: "postgres",
|
|
27
|
+
}),
|
|
19
28
|
});
|
|
20
29
|
|
|
21
30
|
// One read and one write show both important selection decisions. Procedure
|
|
22
31
|
// selection stays in ordinary tests because a third model turn adds cost but
|
|
23
32
|
// does not teach a meaningfully different MCP interaction.
|
|
24
33
|
const fastest = await chat.send("Which snap pea variety matures fastest?");
|
|
25
|
-
|
|
34
|
+
await assertToolCallsWithOptionalFeedback(fastest, [{
|
|
26
35
|
name: "list-pea-varieties",
|
|
27
36
|
arguments: { pea_type: "snap" },
|
|
28
37
|
}]);
|
|
@@ -34,7 +43,7 @@ test("finds and adds pea varieties through natural requests", async (t) => {
|
|
|
34
43
|
"Add Golden Sweet as a climbing mangetout pea that matures in 70 days. " +
|
|
35
44
|
"Use exactly mangetout as its pea type. Its notes are: Purple flowers and flat edible pods.",
|
|
36
45
|
);
|
|
37
|
-
|
|
46
|
+
await assertToolCallsWithOptionalFeedback(added, [{
|
|
38
47
|
name: "add-pea-variety",
|
|
39
48
|
arguments: {
|
|
40
49
|
name: "Golden Sweet",
|
|
@@ -45,4 +54,5 @@ test("finds and adds pea varieties through natural requests", async (t) => {
|
|
|
45
54
|
},
|
|
46
55
|
}]);
|
|
47
56
|
assertResponseContains(added, ["Golden Sweet", "mangetout", "70"]);
|
|
57
|
+
assertNoNegativeFeedback(fastest, added);
|
|
48
58
|
});
|
|
@@ -17,9 +17,10 @@
|
|
|
17
17
|
"lint": "oxlint src test eval scripts kanel.config.cjs"
|
|
18
18
|
},
|
|
19
19
|
"devDependencies": {
|
|
20
|
+
"@emseepea/feedback": "0.2.0",
|
|
20
21
|
"kanel": "4.0.4",
|
|
21
22
|
"kanel-zod": "4.0.1",
|
|
22
|
-
"@emseepea/testing": "0.
|
|
23
|
+
"@emseepea/testing": "0.9.3",
|
|
23
24
|
"@modelcontextprotocol/client": "2.0.0",
|
|
24
25
|
"@types/node": "24.13.3",
|
|
25
26
|
"@types/pg": "8.23.1",
|
|
@@ -31,7 +32,7 @@
|
|
|
31
32
|
},
|
|
32
33
|
"private": true,
|
|
33
34
|
"dependencies": {
|
|
34
|
-
"@emseepea/server": "0.
|
|
35
|
+
"@emseepea/server": "0.6.1",
|
|
35
36
|
"pg": "8.23.0",
|
|
36
37
|
"zod": "4.4.3"
|
|
37
38
|
}
|
|
@@ -1,16 +1,23 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
createEmseepea,
|
|
3
|
+
discoverCapabilities,
|
|
4
|
+
type AccessPolicy,
|
|
5
|
+
type EmseepeaExtensions,
|
|
6
|
+
} from "@emseepea/server";
|
|
2
7
|
import { Pool } from "pg";
|
|
3
8
|
import { z } from "zod";
|
|
4
9
|
|
|
5
|
-
export interface DatabaseSchemaExampleOptions {
|
|
10
|
+
export interface DatabaseSchemaExampleOptions extends EmseepeaExtensions {
|
|
6
11
|
readonly databaseUrl: string;
|
|
12
|
+
readonly access?: AccessPolicy;
|
|
7
13
|
}
|
|
8
14
|
|
|
9
15
|
export async function createDatabaseSchemaExample(options: DatabaseSchemaExampleOptions) {
|
|
16
|
+
const { databaseUrl: databaseUrlOption, access = { access: "public" }, ...extensions } = options;
|
|
10
17
|
const databaseUrl = z.string().url().refine(
|
|
11
18
|
(value) => ["postgres:", "postgresql:"].includes(new URL(value).protocol),
|
|
12
19
|
"databaseUrl must use PostgreSQL",
|
|
13
|
-
).parse(
|
|
20
|
+
).parse(databaseUrlOption);
|
|
14
21
|
let database: Pool | undefined = new Pool({
|
|
15
22
|
connectionString: databaseUrl,
|
|
16
23
|
connectionTimeoutMillis: 2_000,
|
|
@@ -41,7 +48,9 @@ export async function createDatabaseSchemaExample(options: DatabaseSchemaExample
|
|
|
41
48
|
readinessTimeoutMs: 2_500,
|
|
42
49
|
...await discoverCapabilities(new URL("./capabilities/", import.meta.url), {
|
|
43
50
|
database: () => database,
|
|
51
|
+
access,
|
|
44
52
|
}),
|
|
53
|
+
...extensions,
|
|
45
54
|
});
|
|
46
55
|
const closeProvider = async () => {
|
|
47
56
|
const activeDatabase = database;
|
|
@@ -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 { peaVarietyCatalog } from "../generated/public/PeaVarietyCatalog.mjs";
|
|
3
3
|
import { varietySchema } from "../pea-variety.js";
|
|
4
4
|
import type { DatabaseSchemaContext } from "./context.js";
|
|
@@ -8,11 +8,11 @@ const outputSchema = varietySchema;
|
|
|
8
8
|
|
|
9
9
|
export default ((context) => defineTool({
|
|
10
10
|
name: "add-pea-variety",
|
|
11
|
-
access
|
|
11
|
+
...context.access,
|
|
12
12
|
description: "Add one pea variety to the catalogue.",
|
|
13
13
|
inputSchema,
|
|
14
14
|
outputSchema,
|
|
15
|
-
async handler(variety, { signal }) {
|
|
15
|
+
async handler(variety, { signal }: ToolContext) {
|
|
16
16
|
const database = context.database();
|
|
17
17
|
if (!database) throw new Error("Variety provider unavailable");
|
|
18
18
|
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 { peaVarietyCatalog } from "../generated/public/PeaVarietyCatalog.mjs";
|
|
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 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 database = context.database();
|
|
23
23
|
if (!database) throw new Error("Variety provider unavailable");
|
|
24
24
|
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 { peaCatalogSummary } from "../generated/public/PeaCatalogSummary.mjs";
|
|
4
4
|
import type { DatabaseSchemaContext } from "./context.js";
|
|
@@ -16,11 +16,11 @@ const outputSchema = z.object({
|
|
|
16
16
|
|
|
17
17
|
export default ((context) => defineTool({
|
|
18
18
|
name: "summarize-pea-catalog",
|
|
19
|
-
access
|
|
19
|
+
...context.access,
|
|
20
20
|
description: "Summarize the pea catalogue.",
|
|
21
21
|
inputSchema,
|
|
22
22
|
outputSchema,
|
|
23
|
-
async handler({ pea_type }, { signal }) {
|
|
23
|
+
async handler({ pea_type }, { signal }: ToolContext) {
|
|
24
24
|
const database = context.database();
|
|
25
25
|
if (!database) throw new Error("Variety provider unavailable");
|
|
26
26
|
signal.throwIfAborted();
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import test, { after } from "node:test";
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
insecureTestAuthentication,
|
|
6
|
+
startEmseepea,
|
|
7
|
+
startMcpServer,
|
|
8
|
+
} from "@emseepea/testing";
|
|
5
9
|
import { Pool } from "pg";
|
|
10
|
+
import { createDatabaseSchemaExample } from "../dist/app.js";
|
|
6
11
|
|
|
7
12
|
const databaseUrl = process.env.DATABASE_URL;
|
|
8
13
|
assert.ok(databaseUrl, "DATABASE_URL is required");
|
|
@@ -74,6 +79,22 @@ test("reads and writes through the view and summarizes through the procedure", a
|
|
|
74
79
|
);
|
|
75
80
|
});
|
|
76
81
|
|
|
82
|
+
test("the same template composes protected access and observability", async (t) => {
|
|
83
|
+
const events = [];
|
|
84
|
+
const permissions = ["catalogue:write"];
|
|
85
|
+
const { app } = await createDatabaseSchemaExample({
|
|
86
|
+
databaseUrl,
|
|
87
|
+
access: { access: "protected", requiredScopes: permissions },
|
|
88
|
+
authentication: insecureTestAuthentication(permissions),
|
|
89
|
+
observability: [{ id: "test-log", emit: (event) => events.push(event) }],
|
|
90
|
+
});
|
|
91
|
+
const running = await startEmseepea(t, app);
|
|
92
|
+
const client = await running.connect("test-token");
|
|
93
|
+
const result = await client.callTool({ name: "list-pea-varieties", arguments: {} });
|
|
94
|
+
assert.equal(result.isError, false);
|
|
95
|
+
assert.ok(events.some(({ capability }) => capability === "list-pea-varieties"));
|
|
96
|
+
});
|
|
97
|
+
|
|
77
98
|
test("the catalogue read has a fixed 20-row boundary", async (t) => {
|
|
78
99
|
await database.query(`
|
|
79
100
|
INSERT INTO pea_variety_catalog
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@emseepea/create-database-schema-server",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.5",
|
|
4
4
|
"description": "Create an Em See Pea server with internal validation generated from a PostgreSQL schema.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -21,12 +21,13 @@
|
|
|
21
21
|
"prepack": "npm run build:initializer"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
|
-
"@emseepea/
|
|
24
|
+
"@emseepea/feedback": "0.2.0",
|
|
25
|
+
"@emseepea/server": "0.6.1",
|
|
25
26
|
"pg": "8.23.0",
|
|
26
27
|
"zod": "4.4.3",
|
|
27
28
|
"kanel": "4.0.4",
|
|
28
29
|
"kanel-zod": "4.0.1",
|
|
29
|
-
"@emseepea/testing": "0.
|
|
30
|
+
"@emseepea/testing": "0.9.3",
|
|
30
31
|
"@modelcontextprotocol/client": "2.0.0",
|
|
31
32
|
"@types/node": "24.13.3",
|
|
32
33
|
"@types/pg": "8.23.1",
|