@emseepea/create-multi-instance-postgres-server 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,115 @@
1
+ # `@emseepea/create-multi-instance-postgres-server`
2
+
3
+ This directory is the maintained example and the candidate source for its
4
+ public npm initializer.
5
+
6
+ ## Use This Template
7
+
8
+ Use this template when independently deployed server instances must share one
9
+ PostgreSQL-backed store and repeated request IDs must resolve to one stored
10
+ result. Choose the [tool server](../tool-server/README.md) when one process and
11
+ in-memory data are enough. [Compare all eight templates](https://emseepea.github.io/emseepea/examples/).
12
+
13
+ ## Create a Project
14
+
15
+ Publication is pending exact Quality checks. After publication, use:
16
+
17
+ ```sh
18
+ npm init @emseepea/multi-instance-postgres-server -- my-server
19
+ ```
20
+
21
+ <!-- generated-project-readme -->
22
+
23
+ ## Multi-Instance PostgreSQL Server
24
+
25
+ Choose this project when separate server instances may receive the same retry
26
+ and must avoid creating duplicate stored work.
27
+
28
+ Each server has its own Em See Pea app and PostgreSQL connection pool. The
29
+ servers coordinate through one shared database without sharing a local
30
+ filesystem.
31
+
32
+ The same request ID creates one stored pea harvest report. A retry through any
33
+ instance returns the original report, including the name of the instance that
34
+ created it.
35
+
36
+ ## Run Locally
37
+
38
+ You need Node.js 22 or 24 and Docker Compose. Install dependencies, then start
39
+ PostgreSQL and both server instances with one run command:
40
+
41
+ ```sh
42
+ npm install
43
+ npm run dev
44
+ ```
45
+
46
+ The run command applies the database schema, starts two independent server
47
+ processes, and prints both MCP addresses. Stop them with Control-C. Remove the
48
+ database container and local volume when you no longer need them:
49
+
50
+ ```sh
51
+ docker compose down --volumes
52
+ ```
53
+
54
+ ## Use Managed PostgreSQL
55
+
56
+ Set `DATABASE_URL` to a PostgreSQL connection string that every deployed
57
+ instance can reach. Apply the included schema once, then start each instance
58
+ with its own name:
59
+
60
+ ```sh
61
+ npm run build
62
+ npm run db:setup
63
+ EMSEEPEA_INSTANCE=instance-a PORT=3000 npm run start:instance
64
+ ```
65
+
66
+ Run the final command for each deployed instance, changing
67
+ `EMSEEPEA_INSTANCE` for each instance. Protect `DATABASE_URL` as a secret. Do
68
+ not put it in source control or send it to an MCP client.
69
+
70
+ ## Tools
71
+
72
+ - `create-shared-harvest-report` atomically creates or returns one stored report
73
+ for a request ID. The result identifies its original server instance.
74
+ - `describe-instance` returns the instance handling the current request and
75
+ does not query PostgreSQL.
76
+
77
+ PostgreSQL enforces one report per request ID with a unique constraint. The
78
+ tool uses one atomic upsert, without an application-side existence check or a
79
+ distributed lock.
80
+
81
+ If PostgreSQL is unavailable, `/readyz` returns 503 and the report tool returns
82
+ a generic failure. Independent endpoints and `describe-instance` remain
83
+ available for diagnosis.
84
+
85
+ ## Exact Scope
86
+
87
+ - Independently deployable server instances connected to one PostgreSQL database.
88
+ - One bounded connection pool per process.
89
+ - One atomic database statement per report request.
90
+ - PostgreSQL bounds database statements to 1.5 seconds. The driver cannot
91
+ cancel an in-flight query from an AbortSignal, so the database timeout is the
92
+ cancellation boundary.
93
+ - No claim that an external service change happens exactly once.
94
+ - No latency, throughput, or unlimited-scale claim.
95
+
96
+ ## Check This Project
97
+
98
+ [Ordinary tests](test/) live in `test/`. The
99
+ [AI tool-choice and understanding test](eval/meaning.test.mjs) lives separately
100
+ in `eval/`. Docker Compose is required because both suites exercise real
101
+ PostgreSQL behavior.
102
+
103
+ Run the build and two-server MCP checks:
104
+
105
+ ```sh
106
+ npm test
107
+ ```
108
+
109
+ Check that Claude understands report replay correctly:
110
+
111
+ ```sh
112
+ npm run test:llm
113
+ ```
114
+
115
+ 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,93 @@
1
+ # Multi-Instance PostgreSQL Server
2
+
3
+ Choose this project when separate server instances may receive the same retry
4
+ and must avoid creating duplicate stored work.
5
+
6
+ Each server has its own Em See Pea app and PostgreSQL connection pool. The
7
+ servers coordinate through one shared database without sharing a local
8
+ filesystem.
9
+
10
+ The same request ID creates one stored pea harvest report. A retry through any
11
+ instance returns the original report, including the name of the instance that
12
+ created it.
13
+
14
+ ## Run Locally
15
+
16
+ You need Node.js 22 or 24 and Docker Compose. Install dependencies, then start
17
+ PostgreSQL and both server instances with one run command:
18
+
19
+ ```sh
20
+ npm install
21
+ npm run dev
22
+ ```
23
+
24
+ The run command applies the database schema, starts two independent server
25
+ processes, and prints both MCP addresses. Stop them with Control-C. Remove the
26
+ database container and local volume when you no longer need them:
27
+
28
+ ```sh
29
+ docker compose down --volumes
30
+ ```
31
+
32
+ ## Use Managed PostgreSQL
33
+
34
+ Set `DATABASE_URL` to a PostgreSQL connection string that every deployed
35
+ instance can reach. Apply the included schema once, then start each instance
36
+ with its own name:
37
+
38
+ ```sh
39
+ npm run build
40
+ npm run db:setup
41
+ EMSEEPEA_INSTANCE=instance-a PORT=3000 npm run start:instance
42
+ ```
43
+
44
+ Run the final command for each deployed instance, changing
45
+ `EMSEEPEA_INSTANCE` for each instance. Protect `DATABASE_URL` as a secret. Do
46
+ not put it in source control or send it to an MCP client.
47
+
48
+ ## Tools
49
+
50
+ - `create-shared-harvest-report` atomically creates or returns one stored report
51
+ for a request ID. The result identifies its original server instance.
52
+ - `describe-instance` returns the instance handling the current request and
53
+ does not query PostgreSQL.
54
+
55
+ PostgreSQL enforces one report per request ID with a unique constraint. The
56
+ tool uses one atomic upsert, without an application-side existence check or a
57
+ distributed lock.
58
+
59
+ If PostgreSQL is unavailable, `/readyz` returns 503 and the report tool returns
60
+ a generic failure. Independent endpoints and `describe-instance` remain
61
+ available for diagnosis.
62
+
63
+ ## Exact Scope
64
+
65
+ - Independently deployable server instances connected to one PostgreSQL database.
66
+ - One bounded connection pool per process.
67
+ - One atomic database statement per report request.
68
+ - PostgreSQL bounds database statements to 1.5 seconds. The driver cannot
69
+ cancel an in-flight query from an AbortSignal, so the database timeout is the
70
+ cancellation boundary.
71
+ - No claim that an external service change happens exactly once.
72
+ - No latency, throughput, or unlimited-scale claim.
73
+
74
+ ## Check This Project
75
+
76
+ [Ordinary tests](test/) live in `test/`. The
77
+ [AI tool-choice and understanding test](eval/meaning.test.mjs) lives separately
78
+ in `eval/`. Docker Compose is required because both suites exercise real
79
+ PostgreSQL behavior.
80
+
81
+ Run the build and two-server MCP checks:
82
+
83
+ ```sh
84
+ npm test
85
+ ```
86
+
87
+ Check that Claude understands report replay correctly:
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,20 @@
1
+ services:
2
+ database:
3
+ image: postgres:18.6-alpine3.23
4
+ environment:
5
+ POSTGRES_DB: emseepea
6
+ POSTGRES_PASSWORD: emseepea
7
+ POSTGRES_USER: emseepea
8
+ healthcheck:
9
+ test: ["CMD-SHELL", "pg_isready -U emseepea -d emseepea"]
10
+ interval: 1s
11
+ timeout: 3s
12
+ retries: 20
13
+ ports:
14
+ - "127.0.0.1:${POSTGRES_PORT:-5432}:5432"
15
+ volumes:
16
+ - postgres-data:/var/lib/postgresql
17
+ - ./schema.sql:/docker-entrypoint-initdb.d/001-schema.sql:ro
18
+
19
+ volumes:
20
+ postgres-data:
@@ -0,0 +1,44 @@
1
+ import test from "node:test";
2
+ import {
3
+ assertNoToolCalls,
4
+ assertResponseContains,
5
+ assertResponseMeaning,
6
+ assertToolCalls,
7
+ createConversation,
8
+ } from "@emseepea/testing/semantic";
9
+
10
+ const databaseUrl = process.env.DATABASE_URL;
11
+ if (!databaseUrl) throw new Error("DATABASE_URL is required for the PostgreSQL semantic test");
12
+
13
+ test("reuses the original shared report across server instances", async (t) => {
14
+ const chat = await createConversation(t, {
15
+ server: new URL("../dist/server.js", import.meta.url),
16
+ environment: { DATABASE_URL: databaseUrl, EMSEEPEA_INSTANCE: "eval-instance" },
17
+ });
18
+
19
+ // Cross-process concurrency stays in ordinary tests because asking the model
20
+ // to simulate routing would not exercise it. Only the comparison turn needs
21
+ // a semantic judge; the other turns use exact tool and literal assertions.
22
+ const created = await chat.send(
23
+ "Create a shared harvest report with request ID daily-harvest-report.",
24
+ );
25
+ assertToolCalls(created, [
26
+ { name: "create-shared-harvest-report", arguments: { requestId: "daily-harvest-report" } },
27
+ ]);
28
+ const repeated = await chat.send(
29
+ "Create that report again with the same request ID. Is its report ID the " +
30
+ "same as before?",
31
+ );
32
+ assertToolCalls(repeated, [
33
+ { name: "create-shared-harvest-report", arguments: { requestId: "daily-harvest-report" } },
34
+ ]);
35
+ await assertResponseMeaning(repeated, {
36
+ expected: "The repeated request returned the same report ID.",
37
+ });
38
+
39
+ const creator = await chat.send(
40
+ "What exact createdByInstance value did those tool results return?",
41
+ );
42
+ assertNoToolCalls(creator);
43
+ assertResponseContains(creator, "eval-instance");
44
+ });
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "emseepea-starter",
3
+ "version": "0.0.0",
4
+ "description": "Create multiple Em See Pea server instances backed by PostgreSQL.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "scripts": {
8
+ "build": "tsc -p tsconfig.json",
9
+ "start": "node dist/start-two.js",
10
+ "dev": "docker compose up --detach --wait database && npm run build && npm run db:setup && npm start",
11
+ "start:instance": "node dist/server.js",
12
+ "db:setup": "node dist/setup-database.js",
13
+ "test": "npm run build && npm run test:built",
14
+ "test:built": "node test/with-postgres.mjs node --test test/*.test.mjs",
15
+ "test:llm": "npm run build && npm run test:llm:built",
16
+ "test:llm:built": "node test/with-postgres.mjs emseepea-test eval",
17
+ "lint": "oxlint src test eval"
18
+ },
19
+ "devDependencies": {
20
+ "@emseepea/testing": "0.5.2",
21
+ "@modelcontextprotocol/client": "2.0.0",
22
+ "@types/node": "24.13.3",
23
+ "@types/pg": "8.23.1",
24
+ "typescript": "6.0.3",
25
+ "oxlint": "1.80.0"
26
+ },
27
+ "engines": {
28
+ "node": ">=22.13.0"
29
+ },
30
+ "private": true,
31
+ "dependencies": {
32
+ "@emseepea/server": "0.3.3",
33
+ "pg": "8.23.0",
34
+ "zod": "4.4.3"
35
+ }
36
+ }
@@ -0,0 +1,20 @@
1
+ CREATE TABLE IF NOT EXISTS pea_plants (
2
+ name text PRIMARY KEY,
3
+ pea_type text NOT NULL CHECK (pea_type IN ('shelling', 'snap'))
4
+ );
5
+
6
+ INSERT INTO pea_plants (name, pea_type) VALUES
7
+ ('Harbour Gem', 'shelling'),
8
+ ('Highland Snap', 'snap'),
9
+ ('Meadow Sweet', 'snap'),
10
+ ('Garden Pearl', 'shelling')
11
+ ON CONFLICT (name) DO NOTHING;
12
+
13
+ CREATE TABLE IF NOT EXISTS reports (
14
+ report_id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
15
+ idempotency_key text NOT NULL UNIQUE,
16
+ created_by_instance text NOT NULL,
17
+ total_plants integer NOT NULL,
18
+ shelling_count integer NOT NULL,
19
+ snap_count integer NOT NULL
20
+ );
@@ -0,0 +1,56 @@
1
+ import { createEmseepea, discoverCapabilities } from "@emseepea/server";
2
+ import { Pool } from "pg";
3
+ import { z } from "zod";
4
+
5
+ export interface MultiInstanceExampleOptions {
6
+ readonly databaseUrl: string;
7
+ readonly instanceName: string;
8
+ }
9
+
10
+ export async function createMultiInstanceExample(options: MultiInstanceExampleOptions) {
11
+ const instanceName = z.string().min(1).max(64).parse(options.instanceName);
12
+ const databaseUrl = z.string().url().refine(
13
+ (value) => ["postgres:", "postgresql:"].includes(new URL(value).protocol),
14
+ "databaseUrl must use PostgreSQL",
15
+ ).parse(options.databaseUrl);
16
+ let database: Pool | undefined = new Pool({
17
+ connectionString: databaseUrl,
18
+ connectionTimeoutMillis: 2_000,
19
+ idleTimeoutMillis: 10_000,
20
+ max: 4,
21
+ query_timeout: 2_000,
22
+ statement_timeout: 1_500,
23
+ });
24
+ database.on("error", () => {
25
+ // Readiness and tool calls report provider failure without stopping unrelated features.
26
+ });
27
+
28
+ const app = createEmseepea({
29
+ name: "emseepea-multi-instance-postgres-server",
30
+ version: "0.0.0",
31
+ instructions: "Use create-shared-harvest-report for a stored pea harvest report. Reusing a request ID returns the original report.",
32
+ readiness: async ({ signal }) => {
33
+ if (!database) return false;
34
+ try {
35
+ signal.throwIfAborted();
36
+ await database.query("SELECT 1 FROM reports LIMIT 1");
37
+ signal.throwIfAborted();
38
+ return true;
39
+ } catch {
40
+ return false;
41
+ }
42
+ },
43
+ readinessTimeoutMs: 2_500,
44
+ ...await discoverCapabilities(new URL("./capabilities/", import.meta.url), {
45
+ database: () => database,
46
+ instanceName,
47
+ }),
48
+ });
49
+ const closeProvider = async () => {
50
+ const activeDatabase = database;
51
+ database = undefined;
52
+ await activeDatabase?.end();
53
+ };
54
+ app.addHook("onClose", closeProvider);
55
+ return { app, closeProvider };
56
+ }
@@ -0,0 +1,6 @@
1
+ import type { Pool } from "pg";
2
+
3
+ export interface MultiInstanceContext {
4
+ readonly database: () => Pool | undefined;
5
+ readonly instanceName: string;
6
+ }
@@ -0,0 +1,81 @@
1
+ import { defineMappedTool, type CapabilityModuleFactory } from "@emseepea/server";
2
+ import { z } from "zod";
3
+ import type { MultiInstanceContext } from "./context.js";
4
+
5
+ const requestIdSchema = z.string().min(3).max(64).regex(/^[a-z0-9][a-z0-9-]*$/)
6
+ .describe("Idempotency key. Reusing it returns the existing report instead of creating another.");
7
+ const inputSchema = z.object({ requestId: requestIdSchema });
8
+ const outputSchema = z.object({
9
+ reportId: z.number().int().positive().describe("Stored report identifier."),
10
+ requestId: requestIdSchema,
11
+ createdByInstance: z.string().min(1).max(64).describe("Server instance that originally created the report."),
12
+ totalPlants: z.number().int().nonnegative().describe("Total pea plants counted in the report."),
13
+ peaTypeCounts: z.object({
14
+ shelling: z.number().int().nonnegative().describe("Shelling pea plants counted in the report."),
15
+ snap: z.number().int().nonnegative().describe("Snap pea plants counted in the report."),
16
+ }).describe("Plant counts grouped by pea type."),
17
+ });
18
+ const backendInputSchema = z.object({ idempotency_key: requestIdSchema });
19
+ const backendOutputSchema = z.object({
20
+ report_id: z.number().int().positive(),
21
+ idempotency_key: requestIdSchema,
22
+ created_by_instance: z.string().min(1).max(64),
23
+ total_plants: z.number().int().nonnegative(),
24
+ shelling_count: z.number().int().nonnegative(),
25
+ snap_count: z.number().int().nonnegative(),
26
+ });
27
+
28
+ export default ((context) => defineMappedTool({
29
+ name: "create-shared-harvest-report",
30
+ access: "public",
31
+ description: "Create or return one stored pea harvest report per request ID. The result identifies its original server instance.",
32
+ inputSchema,
33
+ outputSchema,
34
+ backendInputSchema,
35
+ backendOutputSchema,
36
+ isAvailable: () => {
37
+ return context.database() !== undefined;
38
+ },
39
+ mapInput: ({ requestId }) => ({ idempotency_key: requestId }),
40
+ async adapter({ idempotency_key }, { signal }) {
41
+ const database = context.database();
42
+ if (!database) throw new Error("Report provider unavailable");
43
+ signal.throwIfAborted();
44
+ const result = await database.query({
45
+ text: `
46
+ INSERT INTO reports (
47
+ idempotency_key, created_by_instance, total_plants,
48
+ shelling_count, snap_count
49
+ )
50
+ SELECT
51
+ $1,
52
+ $2,
53
+ COUNT(*)::integer,
54
+ COUNT(*) FILTER (WHERE pea_type = 'shelling')::integer,
55
+ COUNT(*) FILTER (WHERE pea_type = 'snap')::integer
56
+ FROM pea_plants
57
+ ON CONFLICT (idempotency_key) DO UPDATE
58
+ SET idempotency_key = EXCLUDED.idempotency_key
59
+ RETURNING
60
+ report_id, idempotency_key, created_by_instance, total_plants,
61
+ shelling_count, snap_count
62
+ `,
63
+ values: [idempotency_key, context.instanceName],
64
+ });
65
+ signal.throwIfAborted();
66
+ return result.rows[0] as z.input<typeof backendOutputSchema>;
67
+ },
68
+ mapOutput: (report) => {
69
+ const data = {
70
+ reportId: report.report_id,
71
+ requestId: report.idempotency_key,
72
+ createdByInstance: report.created_by_instance,
73
+ totalPlants: report.total_plants,
74
+ peaTypeCounts: {
75
+ shelling: report.shelling_count,
76
+ snap: report.snap_count,
77
+ },
78
+ };
79
+ return { data };
80
+ },
81
+ })) satisfies CapabilityModuleFactory<MultiInstanceContext>;
@@ -0,0 +1,14 @@
1
+ import { defineTool, type CapabilityModuleFactory } from "@emseepea/server";
2
+ import { z } from "zod";
3
+ import type { MultiInstanceContext } from "./context.js";
4
+
5
+ export default (({ instanceName }) => defineTool({
6
+ name: "describe-instance",
7
+ access: "public",
8
+ description: "Return the server instance handling this request, not the instance that created a stored report.",
9
+ inputSchema: z.object({}),
10
+ outputSchema: z.object({
11
+ instanceName: z.string().describe("Server instance that handled this request."),
12
+ }),
13
+ handler: () => ({ data: { instanceName } }),
14
+ })) satisfies CapabilityModuleFactory<MultiInstanceContext>;
@@ -0,0 +1,29 @@
1
+ import { serveEmseepea } from "@emseepea/server";
2
+ import { createMultiInstanceExample } from "./app.js";
3
+
4
+ const instanceName = process.env.EMSEEPEA_INSTANCE ?? `instance-${process.pid}`;
5
+ const databaseUrl = process.env.DATABASE_URL ?? "postgres://emseepea:emseepea@127.0.0.1:5432/emseepea";
6
+ const { app, closeProvider } = await createMultiInstanceExample({ databaseUrl, instanceName });
7
+ const running = await serveEmseepea(app, {
8
+ port: Number.parseInt(process.env.PORT ?? "3000", 10),
9
+ });
10
+
11
+ console.log(`Em See Pea multi-instance-postgres-server example ${instanceName} listening at ${running.url}`);
12
+ process.send?.({ type: "ready", instanceName, url: running.url.href });
13
+ process.on("message", (message) => {
14
+ if (message === "close-provider") {
15
+ void closeProvider().then(() => process.send?.({ type: "provider-closed", instanceName }));
16
+ }
17
+ });
18
+
19
+ let shuttingDown = false;
20
+ async function shutdown(): Promise<void> {
21
+ if (shuttingDown) return;
22
+ shuttingDown = true;
23
+ await running.close();
24
+ if (process.connected) process.disconnect();
25
+ process.exitCode = 0;
26
+ }
27
+
28
+ process.once("SIGINT", () => void shutdown());
29
+ process.once("SIGTERM", () => void shutdown());
@@ -0,0 +1,13 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { Pool } from "pg";
3
+ import { z } from "zod";
4
+
5
+ const databaseUrl = z.string().url().parse(process.env.DATABASE_URL);
6
+ const schema = await readFile(new URL("../schema.sql", import.meta.url), "utf8");
7
+ const database = new Pool({ connectionString: databaseUrl, max: 1 });
8
+ try {
9
+ await database.query(schema);
10
+ console.log("PostgreSQL schema ready");
11
+ } finally {
12
+ await database.end();
13
+ }
@@ -0,0 +1,32 @@
1
+ import { fork, type ChildProcess } from "node:child_process";
2
+ import { fileURLToPath } from "node:url";
3
+
4
+ const databaseUrl = process.env.DATABASE_URL ?? "postgres://emseepea:emseepea@127.0.0.1:5432/emseepea";
5
+ const serverPath = fileURLToPath(new URL("./server.js", import.meta.url));
6
+ const children = [start("instance-a"), start("instance-b")];
7
+
8
+ function start(instanceName: string): ChildProcess {
9
+ return fork(serverPath, [], {
10
+ env: {
11
+ ...process.env,
12
+ DATABASE_URL: databaseUrl,
13
+ EMSEEPEA_INSTANCE: instanceName,
14
+ PORT: "0",
15
+ },
16
+ stdio: ["inherit", "inherit", "inherit", "ipc"],
17
+ });
18
+ }
19
+
20
+ let stopping = false;
21
+ async function shutdown(): Promise<void> {
22
+ if (stopping) return;
23
+ stopping = true;
24
+ for (const child of children) child.kill("SIGTERM");
25
+ await Promise.all(children.map((child) => new Promise<void>((resolve) => {
26
+ if (child.exitCode !== null) resolve();
27
+ else child.once("exit", () => resolve());
28
+ })));
29
+ }
30
+
31
+ process.once("SIGINT", () => void shutdown());
32
+ process.once("SIGTERM", () => void shutdown());
@@ -0,0 +1,241 @@
1
+ import assert from "node:assert/strict";
2
+ import { fork } from "node:child_process";
3
+ import { fileURLToPath } from "node:url";
4
+ import test, { after } from "node:test";
5
+ import { Client, StreamableHTTPClientTransport } from "@modelcontextprotocol/client";
6
+ import { Pool } from "pg";
7
+
8
+ const serverPath = fileURLToPath(new URL("../dist/server.js", import.meta.url));
9
+ const databaseUrl = process.env.DATABASE_URL;
10
+ assert.ok(databaseUrl, "DATABASE_URL is required");
11
+ const database = new Pool({ connectionString: databaseUrl, max: 2 });
12
+ after(() => database.end());
13
+ const requestMeta = {
14
+ "io.modelcontextprotocol/protocolVersion": "2026-07-28",
15
+ "io.modelcontextprotocol/clientInfo": { name: "multi-instance-test", version: "0.0.0" },
16
+ "io.modelcontextprotocol/clientCapabilities": {},
17
+ };
18
+
19
+ test("two server processes share one atomic report store", async (t) => {
20
+ const first = await startInstance("instance-a", databaseUrl);
21
+ const second = await startInstance("instance-b", databaseUrl);
22
+ const firstClient = await connect(first.url);
23
+ const secondClient = await connect(second.url);
24
+ t.after(async () => {
25
+ await Promise.allSettled([firstClient.close(), secondClient.close()]);
26
+ await Promise.all([stopInstance(first.child), stopInstance(second.child)]);
27
+ });
28
+
29
+ const localRace = await Promise.all(Array.from({ length: 6 }, () => (
30
+ createReport(firstClient, "single-instance-race")
31
+ )));
32
+ assert.equal(new Set(localRace.map(({ reportId }) => reportId)).size, 1);
33
+ assert.equal(await reportCount("single-instance-race"), 1);
34
+
35
+ const [fromFirst, fromSecond] = await Promise.all([
36
+ createReport(firstClient, "shared-instance-race"),
37
+ createReport(secondClient, "shared-instance-race"),
38
+ ]);
39
+ assert.deepEqual(fromFirst, fromSecond);
40
+ assert.match(fromFirst.createdByInstance, /^instance-[ab]$/);
41
+ assert.deepEqual(fromFirst.peaTypeCounts, { shelling: 2, snap: 2 });
42
+ assert.equal(fromFirst.totalPlants, 4);
43
+ assert.equal(await reportCount("shared-instance-race"), 1);
44
+
45
+ const replay = await createReport(
46
+ fromFirst.createdByInstance === "instance-a" ? secondClient : firstClient,
47
+ "shared-instance-race",
48
+ );
49
+ assert.deepEqual(replay, fromFirst);
50
+
51
+ const raw = await rawCreateReport(first.url, "raw-http-report");
52
+ assert.equal(raw.response.status, 200);
53
+ assert.equal(raw.body.result.isError, false);
54
+ assert.equal(raw.body.result.structuredContent.requestId, "raw-http-report");
55
+ assert.equal(raw.body.result.content[0].text, JSON.stringify(raw.body.result.structuredContent));
56
+ assert.equal(await reportCount("raw-http-report"), 1);
57
+
58
+ await closeProvider(first.child);
59
+ const unavailable = await rawCreateReport(first.url, "must-not-be-created");
60
+ assert.equal(unavailable.response.status, 200);
61
+ assert.equal(unavailable.body.result.content[0].text, "Tool execution failed");
62
+ assert.doesNotMatch(
63
+ JSON.stringify({ ...unavailable.body.result, _meta: undefined }),
64
+ /postgres|database|connection|provider/i,
65
+ );
66
+ assert.equal(await reportCount("must-not-be-created"), 0);
67
+
68
+ const independent = await firstClient.callTool({ name: "describe-instance", arguments: {} });
69
+ assert.deepEqual(independent.structuredContent, { instanceName: "instance-a" });
70
+ const readiness = await fetch(new URL("/readyz", first.url));
71
+ assert.equal(readiness.status, 503);
72
+ assert.equal(await readiness.text(), "not ready\n");
73
+
74
+ const secondStillWorks = await createReport(secondClient, "provider-b-still-works");
75
+ assert.equal(secondStillWorks.requestId, "provider-b-still-works");
76
+ });
77
+
78
+ test("describes every multi-instance tool property", async (t) => {
79
+ const instance = await startInstance("schema-instance", databaseUrl);
80
+ const client = await connect(instance.url);
81
+ t.after(async () => {
82
+ await client.close();
83
+ await stopInstance(instance.child);
84
+ });
85
+
86
+ const listed = await client.listTools();
87
+ assert.deepEqual(listed.tools.map(({ name }) => name), [
88
+ "create-shared-harvest-report",
89
+ "describe-instance",
90
+ ]);
91
+ const reportInput = listed.tools[0].inputSchema.properties;
92
+ const reportOutput = listed.tools[0].outputSchema.properties;
93
+ assert.equal(reportInput.requestId.description, "Idempotency key. Reusing it returns the existing report instead of creating another.");
94
+ assert.equal(reportOutput.reportId.description, "Stored report identifier.");
95
+ assert.equal(reportOutput.requestId.description, reportInput.requestId.description);
96
+ assert.equal(reportOutput.createdByInstance.description, "Server instance that originally created the report.");
97
+ assert.equal(reportOutput.totalPlants.description, "Total pea plants counted in the report.");
98
+ assert.equal(reportOutput.peaTypeCounts.description, "Plant counts grouped by pea type.");
99
+ assert.equal(reportOutput.peaTypeCounts.properties.shelling.description, "Shelling pea plants counted in the report.");
100
+ assert.equal(reportOutput.peaTypeCounts.properties.snap.description, "Snap pea plants counted in the report.");
101
+ assert.equal(listed.tools[1].outputSchema.properties.instanceName.description, "Server instance that handled this request.");
102
+ });
103
+
104
+ test("an unavailable PostgreSQL provider fails readiness but not independent tools", async (t) => {
105
+ const instance = await startInstance(
106
+ "unavailable-before-start",
107
+ "postgres://emseepea:emseepea@127.0.0.1:1/emseepea",
108
+ );
109
+ const client = await connect(instance.url);
110
+ t.after(async () => {
111
+ await client.close();
112
+ await stopInstance(instance.child);
113
+ });
114
+
115
+ assert.deepEqual(
116
+ (await client.callTool({ name: "describe-instance", arguments: {} })).structuredContent,
117
+ { instanceName: "unavailable-before-start" },
118
+ );
119
+ const unavailable = await rawCreateReport(instance.url, "provider-never-connected");
120
+ assert.equal(unavailable.body.result.content[0].text, "Tool execution failed");
121
+ assert.doesNotMatch(
122
+ JSON.stringify({ ...unavailable.body.result, _meta: undefined }),
123
+ /postgres|database|connection|ECONNREFUSED|provider/i,
124
+ );
125
+ const readiness = await fetch(new URL("/readyz", instance.url));
126
+ assert.equal(readiness.status, 503);
127
+ assert.equal(await readiness.text(), "not ready\n");
128
+ });
129
+
130
+ test("blocked PostgreSQL work finishes at the database timeout", async (t) => {
131
+ const instance = await startInstance("blocked-query", databaseUrl);
132
+ const blocker = await database.connect();
133
+ t.after(async () => {
134
+ await blocker.query("ROLLBACK").catch(() => {});
135
+ blocker.release();
136
+ await stopInstance(instance.child);
137
+ });
138
+ await blocker.query("BEGIN");
139
+ await blocker.query("LOCK TABLE reports IN ACCESS EXCLUSIVE MODE");
140
+
141
+ const started = Date.now();
142
+ const [readiness, report] = await Promise.all([
143
+ fetch(new URL("/readyz", instance.url)),
144
+ rawCreateReport(instance.url, "blocked-report"),
145
+ ]);
146
+
147
+ assert.ok(Date.now() - started < 3_000, "blocked database work exceeded its bounded timeout");
148
+ assert.equal(readiness.status, 503);
149
+ assert.equal(report.body.result.content[0].text, "Tool execution failed");
150
+ await blocker.query("ROLLBACK");
151
+ assert.equal(await reportCount("blocked-report"), 0);
152
+ });
153
+
154
+ async function startInstance(instanceName, connectionString) {
155
+ const child = fork(serverPath, [], {
156
+ env: { ...process.env, DATABASE_URL: connectionString, EMSEEPEA_INSTANCE: instanceName, PORT: "0" },
157
+ stdio: ["ignore", "pipe", "pipe", "ipc"],
158
+ });
159
+ let errors = "";
160
+ child.stderr.on("data", (chunk) => { errors = `${errors}${chunk}`.slice(-2_000); });
161
+ const message = await waitForMessage(child, ({ type }) => type === "ready")
162
+ .catch((error) => { throw new Error(`${error.message}: ${errors}`); });
163
+ return { child, url: new URL(message.url) };
164
+ }
165
+
166
+ async function connect(url) {
167
+ const client = new Client(
168
+ { name: "emseepea-multi-instance-client", version: "0.0.0" },
169
+ { versionNegotiation: { mode: { pin: "2026-07-28" } } },
170
+ );
171
+ await client.connect(new StreamableHTTPClientTransport(url));
172
+ return client;
173
+ }
174
+
175
+ async function createReport(client, requestId) {
176
+ const result = await client.callTool({ name: "create-shared-harvest-report", arguments: { requestId } });
177
+ assert.equal(result.isError, false);
178
+ return result.structuredContent;
179
+ }
180
+
181
+ async function rawCreateReport(url, requestId) {
182
+ const response = await fetch(url, {
183
+ method: "POST",
184
+ headers: {
185
+ Accept: "application/json, text/event-stream",
186
+ "Content-Type": "application/json",
187
+ "MCP-Protocol-Version": "2026-07-28",
188
+ "Mcp-Method": "tools/call",
189
+ "Mcp-Name": "create-shared-harvest-report",
190
+ },
191
+ body: JSON.stringify({
192
+ jsonrpc: "2.0",
193
+ id: crypto.randomUUID(),
194
+ method: "tools/call",
195
+ params: { name: "create-shared-harvest-report", arguments: { requestId }, _meta: requestMeta },
196
+ }),
197
+ });
198
+ return { response, body: await response.json() };
199
+ }
200
+
201
+ async function reportCount(requestId) {
202
+ const result = await database.query(
203
+ "SELECT COUNT(*)::integer AS count FROM reports WHERE idempotency_key = $1",
204
+ [requestId],
205
+ );
206
+ return result.rows[0].count;
207
+ }
208
+
209
+ async function closeProvider(child) {
210
+ const closed = waitForMessage(child, ({ type }) => type === "provider-closed");
211
+ child.send("close-provider");
212
+ await closed;
213
+ }
214
+
215
+ function waitForMessage(child, predicate) {
216
+ return new Promise((resolve, reject) => {
217
+ const timer = setTimeout(() => finish(new Error("child message timed out")), 10_000);
218
+ const onMessage = (message) => {
219
+ if (message && typeof message === "object" && predicate(message)) finish(undefined, message);
220
+ };
221
+ const onExit = (code) => finish(new Error(`child exited ${code}`));
222
+ const finish = (error, message) => {
223
+ clearTimeout(timer);
224
+ child.off("message", onMessage);
225
+ child.off("exit", onExit);
226
+ if (error) reject(error);
227
+ else resolve(message);
228
+ };
229
+ child.on("message", onMessage);
230
+ child.once("exit", onExit);
231
+ });
232
+ }
233
+
234
+ async function stopInstance(child) {
235
+ if (child.exitCode !== null || child.signalCode !== null) return;
236
+ child.kill("SIGTERM");
237
+ await Promise.race([
238
+ new Promise((resolve) => child.once("exit", resolve)),
239
+ new Promise((_, reject) => setTimeout(() => reject(new Error("server child did not stop")), 2_000)),
240
+ ]);
241
+ }
@@ -0,0 +1,59 @@
1
+ import { spawn } from "node:child_process";
2
+ import { createServer } from "node:net";
3
+
4
+ const [command, ...args] = process.argv.slice(2);
5
+ if (!command) throw new Error("A test command is required");
6
+
7
+ const project = `emseepea-test-${process.pid}`;
8
+ const port = await availablePort();
9
+ const composeEnvironment = { ...process.env, POSTGRES_PORT: String(port) };
10
+ const compose = (...composeArgs) => run(
11
+ "docker",
12
+ ["compose", "--project-name", project, ...composeArgs],
13
+ composeEnvironment,
14
+ 120_000,
15
+ );
16
+
17
+ let exitCode = 1;
18
+ try {
19
+ if (await compose("up", "--detach", "--wait", "database") !== 0) {
20
+ throw new Error("Could not start the PostgreSQL test service");
21
+ }
22
+ exitCode = await run(command, args, {
23
+ ...process.env,
24
+ DATABASE_URL: `postgres://emseepea:emseepea@127.0.0.1:${port}/emseepea`,
25
+ });
26
+ } finally {
27
+ await compose("down", "--volumes");
28
+ }
29
+ process.exitCode = exitCode;
30
+
31
+ function run(executable, executableArgs, env, timeout = 600_000) {
32
+ return new Promise((resolve, reject) => {
33
+ const child = spawn(executable, executableArgs, { env, stdio: "inherit" });
34
+ const timer = setTimeout(() => child.kill("SIGKILL"), timeout);
35
+ child.once("error", (error) => {
36
+ clearTimeout(timer);
37
+ reject(error);
38
+ });
39
+ child.once("close", (code) => {
40
+ clearTimeout(timer);
41
+ resolve(code ?? 1);
42
+ });
43
+ });
44
+ }
45
+
46
+ function availablePort() {
47
+ return new Promise((resolve, reject) => {
48
+ const server = createServer();
49
+ server.once("error", reject);
50
+ server.listen(0, "127.0.0.1", () => {
51
+ const address = server.address();
52
+ server.close((error) => {
53
+ if (error) reject(error);
54
+ else if (address && typeof address === "object") resolve(address.port);
55
+ else reject(new Error("Could not choose a PostgreSQL test port"));
56
+ });
57
+ });
58
+ });
59
+ }
@@ -0,0 +1,15 @@
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": [
13
+ "src/**/*.ts"
14
+ ]
15
+ }
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "@emseepea/create-multi-instance-postgres-server",
3
+ "version": "0.0.1",
4
+ "description": "Create multiple Em See Pea server instances backed by PostgreSQL.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "starterDependencies": [
8
+ "@emseepea/server",
9
+ "pg",
10
+ "zod"
11
+ ],
12
+ "scripts": {
13
+ "build": "npm run build:example && npm run build:initializer",
14
+ "build:example": "tsc -p tsconfig.json",
15
+ "build:initializer": "node ../../scripts/build-initializer.mjs",
16
+ "start": "node dist/start-two.js",
17
+ "dev": "docker compose up --detach --wait database && npm run build && npm run db:setup && npm start",
18
+ "start:instance": "node dist/server.js",
19
+ "db:setup": "node dist/setup-database.js",
20
+ "test": "npm run build && npm run test:built",
21
+ "test:built": "node test/with-postgres.mjs node --test test/*.test.mjs",
22
+ "test:llm": "npm run build && npm run test:llm:built",
23
+ "test:llm:built": "node test/with-postgres.mjs emseepea-test eval",
24
+ "lint": "oxlint src test eval",
25
+ "prepack": "npm run build:initializer"
26
+ },
27
+ "devDependencies": {
28
+ "@emseepea/server": "0.3.3",
29
+ "@emseepea/testing": "0.5.2",
30
+ "@modelcontextprotocol/client": "2.0.0",
31
+ "@types/node": "24.13.3",
32
+ "@types/pg": "8.23.1",
33
+ "oxlint": "1.80.0",
34
+ "pg": "8.23.0",
35
+ "typescript": "6.0.3",
36
+ "zod": "4.4.3"
37
+ },
38
+ "engines": {
39
+ "node": ">=22.13.0"
40
+ },
41
+ "repository": {
42
+ "type": "git",
43
+ "url": "git+https://github.com/emseepea/emseepea.git",
44
+ "directory": "examples/multi-instance-postgres-server"
45
+ },
46
+ "homepage": "https://emseepea.github.io/emseepea/examples/",
47
+ "bugs": "https://github.com/emseepea/emseepea/issues",
48
+ "publishConfig": {
49
+ "access": "public",
50
+ "provenance": true
51
+ },
52
+ "bin": {
53
+ "create-multi-instance-postgres-server": "./initializer-dist/create.mjs"
54
+ },
55
+ "files": [
56
+ "initializer-dist"
57
+ ]
58
+ }