run-dmcp 0.2.0 → 0.4.0

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.
Files changed (59) hide show
  1. package/README.md +51 -13
  2. package/dist/bin/run-dmcp.js +1 -1
  3. package/dist/db/schema.js +170 -18
  4. package/dist/http/server.js +22 -1
  5. package/dist/index.d.ts +34 -2
  6. package/dist/index.js +86 -2
  7. package/dist/mcp-server.d.ts +1 -1
  8. package/dist/mcp-server.js +1 -1
  9. package/dist/register/resources.js +2 -2
  10. package/dist/rpg/index.d.ts +0 -16
  11. package/dist/rpg/index.js +4 -22
  12. package/dist/rpg/server.d.ts +16 -0
  13. package/dist/rpg/server.js +22 -0
  14. package/dist/schemas/index.d.ts +62 -62
  15. package/dist/server.d.ts +1 -0
  16. package/dist/server.js +20 -0
  17. package/dist/timeline/changes.d.ts +8 -0
  18. package/dist/timeline/changes.js +8 -0
  19. package/dist/timeline/export.d.ts +10 -0
  20. package/dist/timeline/export.js +10 -0
  21. package/dist/timeline/irreversible.d.ts +2 -0
  22. package/dist/timeline/replay.d.ts +22 -0
  23. package/dist/timeline/replay.js +22 -0
  24. package/dist/timeline/schema.js +2 -0
  25. package/dist/tools/audio.js +13 -9
  26. package/dist/tools/game.js +33 -1
  27. package/dist/tools/images.js +17 -10
  28. package/dist/tools/resource.d.ts +2 -2
  29. package/dist/tools/time.js +18 -3
  30. package/dist/types/index.d.ts +1 -1
  31. package/dist/utils/media-path.d.ts +52 -0
  32. package/dist/utils/media-path.js +106 -0
  33. package/dist/utils/output-schemas.d.ts +63 -63
  34. package/dist/utils/output-schemas.js +1 -1
  35. package/package.json +14 -2
  36. package/dist/register/abilities.d.ts +0 -2
  37. package/dist/register/abilities.js +0 -165
  38. package/dist/register/combat.d.ts +0 -2
  39. package/dist/register/combat.js +0 -207
  40. package/dist/register/mcp-prompts.d.ts +0 -2
  41. package/dist/register/mcp-prompts.js +0 -684
  42. package/dist/register/quests.d.ts +0 -2
  43. package/dist/register/quests.js +0 -118
  44. package/dist/register/status.d.ts +0 -2
  45. package/dist/register/status.js +0 -130
  46. package/dist/register/tables.d.ts +0 -2
  47. package/dist/register/tables.js +0 -146
  48. package/dist/tools/ability.d.ts +0 -48
  49. package/dist/tools/ability.js +0 -238
  50. package/dist/tools/combat.d.ts +0 -13
  51. package/dist/tools/combat.js +0 -195
  52. package/dist/tools/dice.d.ts +0 -23
  53. package/dist/tools/dice.js +0 -111
  54. package/dist/tools/quest.d.ts +0 -34
  55. package/dist/tools/quest.js +0 -164
  56. package/dist/tools/status.d.ts +0 -36
  57. package/dist/tools/status.js +0 -218
  58. package/dist/tools/tables.d.ts +0 -33
  59. package/dist/tools/tables.js +0 -209
package/README.md CHANGED
@@ -12,23 +12,26 @@ holds full adjudication discretion over a world that only ever stores *now*.
12
12
 
13
13
  ## Status
14
14
 
15
- **0.1.1 — the foundation, not the thesis.** What ships today is the predecessor's engine plus four
16
- pieces of generic mechanism that were built for it and offered back to it:
15
+ **0.3.0.** The base is the predecessor's engine plus four pieces of generic mechanism that were built
16
+ for it and offered back to it:
17
17
 
18
18
  - **Atomicity** — `withTransaction()` wired into the multi-write operations that were running
19
19
  non-atomically.
20
20
  - **Declarative constraints** — resources can be declared `bounded` or `monotonic`, and the store
21
- enforces it rather than trusting every caller. (`resolve_only` — every direct write rejected, so a
22
- value can move only through an adjudicating call — exists in a downstream consumer and has not been
23
- extracted here yet. It arrives with the resolve protocol; see [docs/DESIGN.md](docs/DESIGN.md) §5.2a.)
21
+ enforces it rather than trusting every caller. `resolve_only` — every direct write rejected, so a
22
+ value can move only through an adjudicating call — joined them alongside the resolve protocol, and
23
+ is enforced by a trigger rather than by a checker anyone has to remember to call; see
24
+ [docs/DESIGN.md](docs/DESIGN.md) §5.2a.
24
25
  - **Conserved resource sets** — a set of resources can be declared conserved, with an atomic transfer
25
26
  that never silently clamps.
26
27
  - **On-expiry consequences** — scheduled events and timers can carry a consequence that actually
27
28
  lands when they expire, rather than expiring into nothing.
28
29
 
29
- 0.1.1 adds the packaging half of that: importing the library starts nothing, the database resolves to
30
- the consuming application rather than into `node_modules`, and a consumer can bring up its own tables
31
- through the migration hook below.
30
+ 0.1.1 added the packaging half of that: importing the library starts nothing, the database resolves
31
+ to the consuming application rather than into `node_modules`, and a consumer can bring up its own
32
+ tables through the migration hook below. 0.3.0 adds the consuming half — the entity/property spine is
33
+ exported as library functions, so a consumer that deletes its own vendored copy has something to
34
+ import rather than a server to make tool calls into.
32
35
 
33
36
  The timeline that gives this project its reason to exist — interval-versioned facts, `replay(t)`,
34
37
  irreversibility, timeline export, `changes_within` — is built, and every write of world state appends
@@ -56,14 +59,28 @@ schema comes up, where the database lives, and whether anything listens.
56
59
 
57
60
  **The package root is the core, and the tabletop surface is a layer above it.** Dice, combat,
58
61
  abilities, status effects, random tables and quests are genuinely game-shaped — an optional
59
- dependency, not part of the engine (see [docs/DESIGN.md](docs/DESIGN.md) §8). A consumer that only
60
- needs entities, facts, events and the timeline imports `run-dmcp` and calls `createCoreMcpServer`. A
61
- consumer that wants the full tabletop surface imports `run-dmcp/rpg` and calls `createMcpServer`
62
- same name, same options, the whole assembly this package has always served:
62
+ dependency, not part of the engine (see [docs/DESIGN.md](docs/DESIGN.md) §8).
63
+
64
+ **Mechanism and assembly are separate entries, and importing one never loads the other** (since
65
+ 0.4.0). Four specifiers, two layers by two kinds:
66
+
67
+ | | mechanism — functions, constants, types | assembly — builds a server |
68
+ |---|---|---|
69
+ | **core** | `run-dmcp` | `run-dmcp/server` → `createCoreMcpServer` |
70
+ | **+ tabletop** | `run-dmcp/rpg` | `run-dmcp/rpg/server` → `createMcpServer` |
71
+
72
+ Assembling a server means loading the MCP SDK, twenty-one register modules and (for the full
73
+ assembly) the web UI. Wanting `createGame` or `LIMITS` does not, and until 0.4.0 both entries
74
+ charged for it anyway: the core entry cost 97.4ms per process cold and costs 47.0ms now, which is
75
+ below the MCP SDK's own 55.0ms, because without the assembly it no longer loads the SDK at all.
76
+
77
+ A consumer that *does* build a server loads the SDK regardless, so it saves less than that
78
+ difference suggests — measured with the SDK warm, importing both entries, 67.4ms → 38.0ms, about
79
+ 29ms per process. The functions did not change; their specifiers did.
63
80
 
64
81
  ```ts
65
82
  import { initializeSchema, type SchemaMigration } from "run-dmcp";
66
- import { createMcpServer } from "run-dmcp/rpg";
83
+ import { createMcpServer } from "run-dmcp/rpg/server";
67
84
 
68
85
  const migrations: SchemaMigration[] = [
69
86
  {
@@ -93,6 +110,27 @@ The database lands in the consuming application: `DMCP_DB_PATH` if set, else an
93
110
  `~/.local/share/dmcp`, else `./data/games.db` relative to the working directory. Never inside the
94
111
  installed package.
95
112
 
113
+ **The spine is importable, not only callable over a transport.** Games, locations, characters,
114
+ factions, relationships, resources, secrets, items, notes, tags, time and timers are library
115
+ functions first and MCP tools second, so a consumer reads and writes its own world directly and only
116
+ serves the tools it actually wants an LLM to reach. That includes the constrained-write choke point:
117
+ a number that moves through `updateResourceValue` is checked against its declared constraints and its
118
+ history is `facts`, queryable with `valueHistory`.
119
+
120
+ ```ts
121
+ import { createGame, createResource, updateResourceValue, valueHistory } from "run-dmcp";
122
+
123
+ const game = createGame({ name: "The Granary", setting: "…", style: "…" });
124
+ const grain = createResource({ gameId: game.id, ownerType: "game", name: "grain", value: 100 });
125
+
126
+ updateResourceValue({ resourceId: grain.id, mode: "delta", value: -30, reason: "the winter ration" });
127
+ valueHistory(grain.id, "value"); // → the transitions, with the reason each one carried
128
+ ```
129
+
130
+ The annotations, input limits, error envelope and logger the engine's own tools are built from are
131
+ exported too (`ANNOTATIONS`, `LIMITS`, `errors`, `createLogger`), so a consumer's own tools can
132
+ refuse and bound the way these do instead of re-implementing it.
133
+
96
134
  ## Provenance
97
135
 
98
136
  This continues [DMCP](https://github.com/shawnrushefsky/dmcp) by Shawn Rushefsky (MIT), whose last
@@ -18,7 +18,7 @@ import { startHttpServer } from "../http/server.js";
18
18
  // (design §8, issue #17). The application always served the full surface,
19
19
  // so it reaches for it here rather than the core-only `createCoreMcpServer`
20
20
  // in ../mcp-server.js.
21
- import { createMcpServer } from "../rpg/index.js";
21
+ import { createMcpServer } from "../rpg/server.js";
22
22
  import { httpPortFromEnv, setHttpPort, webUiEnabled } from "../utils/webui.js";
23
23
  import { createLogger } from "../utils/logger.js";
24
24
  const log = createLogger("bin");
package/dist/db/schema.js CHANGED
@@ -3,6 +3,49 @@ import { createLogger } from "../utils/logger.js";
3
3
  import { initializeTimelineSchema } from "../timeline/schema.js";
4
4
  import { initializeAdjudicationSchema } from "../timeline/adjudication.js";
5
5
  const log = createLogger("schema");
6
+ /**
7
+ * Run one whole-table CHECK rebuild with foreign-key enforcement suspended,
8
+ * and verify afterwards that nothing was left dangling.
9
+ *
10
+ * SQLite cannot ALTER a CHECK, so widening one means rebuilding the table:
11
+ * copy the rows aside, DROP, CREATE with the new CHECK, copy back. Two
12
+ * migrations in this file do that, and both need the same two guarantees.
13
+ *
14
+ * ENFORCEMENT MUST BE OFF ACROSS THE DROP. With it on, `DROP TABLE` performs
15
+ * an implicit per-row DELETE first, precisely so that any ON DELETE action
16
+ * declared against that table fires as though each row had genuinely been
17
+ * deleted -- which for both of these tables means cascade-emptying the tables
18
+ * that reference them. Suspending it is what makes the drop a schema
19
+ * operation instead of a silent mass deletion. It is toggled out here rather
20
+ * than inside `withTransaction`, because `PRAGMA foreign_keys` is a
21
+ * documented no-op while a transaction is pending.
22
+ *
23
+ * AND IT MUST GO BACK ON WHEN THE REBUILD THROWS, which is why the restore is
24
+ * in a `finally` and why this is a shared function rather than two copies.
25
+ * Both call sites previously restored it on the success path only.
26
+ * `getDatabase()` caches one connection at module scope, so a rebuild that
27
+ * threw handed the rest of the process a handle with foreign keys still
28
+ * disabled -- and a disabled foreign key does not announce itself. It means
29
+ * every ON DELETE CASCADE in this schema quietly stops working: deleting a
30
+ * game orphans its characters, resources, locations and secrets instead of
31
+ * taking them with it, and nothing errors. The scenario is not exotic -- a
32
+ * staging table left behind by a rebuild that died partway is exactly what
33
+ * makes the next startup's `CREATE TABLE ..._staging` throw. Covered by
34
+ * `src/db/__tests__/foreignKeysRestored.test.ts`.
35
+ */
36
+ function rebuildWithForeignKeysSuspended(db, label, rebuild) {
37
+ db.pragma("foreign_keys = OFF");
38
+ try {
39
+ withTransaction(rebuild);
40
+ const violations = db.pragma("foreign_key_check");
41
+ if (violations.length > 0) {
42
+ throw new Error(`${label} CHECK migration left dangling foreign keys: ` + JSON.stringify(violations));
43
+ }
44
+ }
45
+ finally {
46
+ db.pragma("foreign_keys = ON");
47
+ }
48
+ }
6
49
  export function initializeSchema(options) {
7
50
  const db = getDatabase();
8
51
  // ============================================================================
@@ -203,12 +246,22 @@ export function initializeSchema(options) {
203
246
  )
204
247
  `);
205
248
  // Resources table (for tracking currency, reputation, counters, etc.)
206
- db.exec(`
249
+ //
250
+ // `RESOURCES_DDL` is a shared constant, not a literal inlined here, for the
251
+ // same reason `RESOURCE_CONSTRAINTS_DDL` further down is one: the CHECK-
252
+ // rebuild migration immediately below needs this exact text executed in
253
+ // TWO places -- here, for a fresh database, and again inside the rebuild,
254
+ // for a database that still carries the OLD two-member CHECK. Sharing one
255
+ // JS string is what makes "a fresh database and a migrated database
256
+ // converge on byte-identical `sqlite_master.sql` for this table" true BY
257
+ // CONSTRUCTION rather than by two hand-written literals happening to agree
258
+ // today.
259
+ const RESOURCES_DDL = `
207
260
  CREATE TABLE IF NOT EXISTS resources (
208
261
  id TEXT PRIMARY KEY,
209
262
  game_id TEXT NOT NULL,
210
263
  owner_id TEXT,
211
- owner_type TEXT NOT NULL CHECK (owner_type IN ('game', 'character')),
264
+ owner_type TEXT NOT NULL CHECK (owner_type IN ('game', 'character', 'faction', 'location')),
212
265
  name TEXT NOT NULL,
213
266
  description TEXT,
214
267
  category TEXT,
@@ -218,7 +271,115 @@ export function initializeSchema(options) {
218
271
  created_at TEXT NOT NULL,
219
272
  FOREIGN KEY (game_id) REFERENCES games(id) ON DELETE CASCADE
220
273
  )
221
- `);
274
+ `;
275
+ db.exec(RESOURCES_DDL);
276
+ // Migration: widen `resources.owner_type` to admit 'faction' and
277
+ // 'location' alongside 'game' and 'character'. A resource owned by a
278
+ // faction or a location is generic mechanism -- the engine already has
279
+ // `factions` and `locations` tables; this just lets `resources` point at
280
+ // either the way it already points at a `game` or a `character` -- so it
281
+ // belongs here rather than behind a downstream application's own migration.
282
+ //
283
+ // SQLite cannot ALTER a CHECK constraint, so a database that already has
284
+ // `resources` rows under the OLD two-member CHECK needs the same full-
285
+ // table-rebuild recipe the `resource_constraints` CHECK-widening migration
286
+ // uses further down this function (see that block's comment for the
287
+ // detailed reasoning this one leans on): build a replacement table with
288
+ // the new CHECK, copy every row across, drop the old table, put the
289
+ // replacement in its place.
290
+ //
291
+ // DETECTION IS IDEMPOTENT AND LITERAL, not a guess: read this codebase's
292
+ // OWN generated DDL back out of `sqlite_master` and check whether it
293
+ // already contains the token 'faction' -- the same "a literal check for a
294
+ // token we defined in output we generated is fine" carve-out the
295
+ // `resource_constraints` migration's own comment cites (hard rule 4 in the
296
+ // downstream game's own engineering standards; this engine has no
297
+ // narrative-language rule of its own to point at, but the reasoning is the
298
+ // same: this is a substring check against SQL text THIS FUNCTION generated
299
+ // a few lines above, never against anything a player or a model wrote). A
300
+ // truly fresh database never takes the branch below: `RESOURCES_DDL`'s
301
+ // `CREATE TABLE IF NOT EXISTS` a few lines up already carries the widened
302
+ // CHECK, so by the time this runs, this database's own `resources` already
303
+ // contains 'faction'.
304
+ const resourcesDdl = db
305
+ .prepare(`SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'resources'`)
306
+ .get();
307
+ if (resourcesDdl && !resourcesDdl.sql.includes("faction")) {
308
+ // DROP THE PROJECTION TRIGGERS FIRST, UNCONDITIONALLY, IF THEY EXIST.
309
+ // `timeline_resources_ai`/`_au`/`_ad` are defined ON `resources` itself
310
+ // (`AFTER INSERT/UPDATE/DELETE ON resources`), so SQLite drops them
311
+ // automatically the moment `DROP TABLE resources` below runs -- but
312
+ // dropping them here too, explicitly, costs nothing and removes any
313
+ // dependence on that implicit behaviour being exactly right. They are
314
+ // unconditionally reinstalled, generated fresh off the rebuilt table's
315
+ // own `pragma_table_info`, by `installProjectionTriggers()`
316
+ // (`src/timeline/projection.ts`), which `initializeTimelineSchema()`
317
+ // calls LAST in this function -- see the comment on that call for why it
318
+ // runs after every migration above it, this one included.
319
+ db.exec(`DROP TRIGGER IF EXISTS timeline_resources_ai`);
320
+ db.exec(`DROP TRIGGER IF EXISTS timeline_resources_au`);
321
+ db.exec(`DROP TRIGGER IF EXISTS timeline_resources_ad`);
322
+ // Enforcement has to be suspended across the drop, and put back
323
+ // afterwards on every path -- see `rebuildWithForeignKeysSuspended`. Here
324
+ // the tables that would be cascade-emptied are `resource_history` and
325
+ // `resource_constraint_members`, both of which declare
326
+ // `FOREIGN KEY (resource_id) REFERENCES resources(id) ON DELETE CASCADE`.
327
+ rebuildWithForeignKeysSuspended(db, "resources.owner_type", () => {
328
+ // 1. Copy the OLD table's rows into a staging table under a temporary
329
+ // name, already carrying the widened CHECK.
330
+ db.exec(`
331
+ CREATE TABLE resources_staging (
332
+ id TEXT PRIMARY KEY,
333
+ game_id TEXT NOT NULL,
334
+ owner_id TEXT,
335
+ owner_type TEXT NOT NULL CHECK (owner_type IN ('game', 'character', 'faction', 'location')),
336
+ name TEXT NOT NULL,
337
+ description TEXT,
338
+ category TEXT,
339
+ value REAL NOT NULL DEFAULT 0,
340
+ min_value REAL,
341
+ max_value REAL,
342
+ created_at TEXT NOT NULL
343
+ )
344
+ `);
345
+ db.exec(`
346
+ INSERT INTO resources_staging (id, game_id, owner_id, owner_type, name, description, category, value, min_value, max_value, created_at)
347
+ SELECT id, game_id, owner_id, owner_type, name, description, category, value, min_value, max_value, created_at FROM resources
348
+ `);
349
+ // 2. Drop the OLD table outright -- not renamed. Renaming it out of
350
+ // the way first would rewrite `resource_history` and
351
+ // `resource_constraint_members`'s own stored FOREIGN KEY clauses to
352
+ // point at the temporary name (RENAME TO rewrites every OTHER
353
+ // table's FK text that references the renamed table, regardless of
354
+ // the `foreign_keys` pragma), leaving them permanently dangling
355
+ // once that temporary table is dropped a few steps later. `DROP
356
+ // TABLE`, unlike `RENAME TO`, does not rewrite other tables'
357
+ // references -- there is nothing to rewrite them TO -- so this
358
+ // recipe never renames the table other tables' foreign keys point
359
+ // at; the FINAL name is produced by a genuine `CREATE TABLE`
360
+ // instead, and `resource_history`/`resource_constraint_members`'s
361
+ // FK text is never touched by anything in this block.
362
+ db.exec(`DROP TABLE resources`);
363
+ // 3. Recreate under the FINAL name using the exact same DDL text the
364
+ // fresh-database path executed above -- `RESOURCES_DDL` itself, not
365
+ // a second hand-copied literal -- so its stored SQL matches the
366
+ // fresh-database path byte for byte.
367
+ db.exec(RESOURCES_DDL);
368
+ // 4. Copy every row back across from the staging table with an
369
+ // EXPLICIT column list -- never SELECT * -- and drop the staging
370
+ // table. Every id was copied verbatim, so `resource_history` and
371
+ // `resource_constraint_members`'s own `FOREIGN KEY (resource_id)
372
+ // REFERENCES resources(id)` -- never touched by any of the steps
373
+ // above -- is satisfied by the replacement table throughout,
374
+ // verified for real by the `PRAGMA foreign_key_check` below, not
375
+ // merely assumed here.
376
+ db.exec(`
377
+ INSERT INTO resources (id, game_id, owner_id, owner_type, name, description, category, value, min_value, max_value, created_at)
378
+ SELECT id, game_id, owner_id, owner_type, name, description, category, value, min_value, max_value, created_at FROM resources_staging
379
+ `);
380
+ db.exec(`DROP TABLE resources_staging`);
381
+ });
382
+ }
222
383
  // Resource history table (tracks all changes) -- FROZEN, see the trigger
223
384
  // immediately below. Kept for existing rows only; nothing writes here any
224
385
  // more.
@@ -393,15 +554,12 @@ export function initializeSchema(options) {
393
554
  // means it is unconditionally reinstalled before this function returns
394
555
  // regardless -- this is that discipline paying for itself a second time.
395
556
  db.exec(`DROP TRIGGER IF EXISTS timeline_facts_resolve_only`);
396
- // PRAGMA foreign_keys is a documented no-op when toggled inside a
397
- // pending transaction, so it brackets withTransaction() below rather
398
- // than living inside it -- set OFF here (before BEGIN), restored ON
399
- // after COMMIT. connection.ts turns it ON for every connection at
400
- // open (`getDatabase()`); this block is the one place in the codebase
401
- // that deliberately, temporarily, undoes that, and it is responsible
402
- // for putting it back.
403
- db.pragma("foreign_keys = OFF");
404
- withTransaction(() => {
557
+ // Enforcement has to be suspended across the drop, and put back
558
+ // afterwards on every path -- see `rebuildWithForeignKeysSuspended`. Here
559
+ // the table that would be cascade-emptied is `resource_constraint_members`,
560
+ // which declares
561
+ // `FOREIGN KEY (constraint_id) REFERENCES resource_constraints(id) ON DELETE CASCADE`.
562
+ rebuildWithForeignKeysSuspended(db, "resource_constraints (resolve_only, issue #13)", () => {
405
563
  // EMPIRICALLY MEASURED, NOT ASSUMED (see resolveOnly.test.ts, whose
406
564
  // FK-check assertion caught a real bug in an earlier version of this
407
565
  // block): `ALTER TABLE ... RENAME TO` does not just rename the table
@@ -501,12 +659,6 @@ export function initializeSchema(options) {
501
659
  // below, not merely assumed here.
502
660
  db.exec(`DROP TABLE resource_constraints_staging`);
503
661
  });
504
- const fkViolations = db.pragma("foreign_key_check");
505
- if (fkViolations.length > 0) {
506
- throw new Error(`resource_constraints CHECK migration (resolve_only, issue #13) left dangling foreign keys: ` +
507
- JSON.stringify(fkViolations));
508
- }
509
- db.pragma("foreign_keys = ON");
510
662
  }
511
663
  // Members of a resource constraint. 'bounded' and 'monotonic' constraints
512
664
  // have exactly one member (the resource they govern); 'conserved'
@@ -583,7 +583,28 @@ export function createHttpServer(_port = 3456) {
583
583
  next();
584
584
  return;
585
585
  }
586
- res.sendFile(join(CLIENT_DIST, "index.html"));
586
+ // `dotfiles: "allow"` is load-bearing, and its absence was a real bug.
587
+ //
588
+ // res.sendFile() with no `root` option hands the WHOLE absolute path to
589
+ // `send`, whose `dotfiles` default is "ignore" -- so if ANY segment of
590
+ // the path this package is installed under begins with a dot, `send`
591
+ // refuses the file with NotFoundError and the error handler below turns
592
+ // that into a 500. Every client-side route breaks at once, while `/`
593
+ // keeps working (it is served by express.static above, which only
594
+ // dotfile-checks the REQUEST path, not its own root). That is a
595
+ // spectacularly confusing failure: the app is fine, the file is there,
596
+ // and the deep link 500s because of where the checkout happens to live.
597
+ //
598
+ // Installing under a dot directory is ordinary -- a worktree beneath
599
+ // `.claude/`, a deploy under `~/.local/share`, a CI checkout in a dotted
600
+ // cache path -- so this is not a hypothetical.
601
+ //
602
+ // Safe, because the dotfiles guard is protecting nothing here: this path
603
+ // is a server-controlled constant (CLIENT_DIST plus a literal file name)
604
+ // and no part of it comes from the request. The guard exists to stop a
605
+ // user-supplied path from reaching `.env` or `.git`; there is no
606
+ // user-supplied path in it.
607
+ res.sendFile(join(CLIENT_DIST, "index.html"), { dotfiles: "allow" });
587
608
  });
588
609
  }
589
610
  else {
package/dist/index.d.ts CHANGED
@@ -1,4 +1,3 @@
1
- export { createCoreMcpServer, SERVER_NAME, SERVER_VERSION } from "./mcp-server.js";
2
1
  export { getDatabase, closeDatabase, withTransaction, getDatabasePath, getDataDir, resolveDataPathFrom, } from "./db/connection.js";
3
2
  export type { DataPathInputs } from "./db/connection.js";
4
3
  export { initializeSchema } from "./db/schema.js";
@@ -32,5 +31,38 @@ export { exportTimeline, importTimeline, exportTimelineToFile, importTimelineFro
32
31
  export type { TimelineExport, TimelineExportEntity, TimelineExportFact, TimelineExportEvent, TimelineExportClock, TimelineImportResult, } from "./timeline/export.js";
33
32
  export { ENTITY_KINDS } from "./timeline/kinds.js";
34
33
  export type { EntityKind } from "./timeline/kinds.js";
35
- export { DEFAULT_HTTP_PORT, httpPortFromEnv, webUiEnabled, setHttpPort, getWebUiBaseUrl, } from "./utils/webui.js";
34
+ export { DEFAULT_HTTP_PORT, httpPortFromEnv, webUiEnabled, setHttpPort, getWebUiBaseUrl, getGameUrl, getCharacterUrl, getLocationUrl, } from "./utils/webui.js";
35
+ export * from "./tools/game.js";
36
+ export * from "./tools/world.js";
37
+ export * from "./tools/character.js";
38
+ export * from "./tools/faction.js";
39
+ export * from "./tools/relationship.js";
40
+ export * from "./tools/resource.js";
41
+ export * from "./tools/constraint.js";
42
+ export * from "./tools/inventory.js";
43
+ export * from "./tools/secrets.js";
44
+ export * from "./tools/narrative.js";
45
+ export * from "./tools/notes.js";
46
+ export * from "./tools/tags.js";
47
+ export * from "./tools/time.js";
48
+ export * from "./tools/timers.js";
49
+ export * from "./tools/rules.js";
50
+ export * from "./tools/pause.js";
51
+ export * from "./tools/display.js";
52
+ export * from "./tools/images.js";
53
+ export * from "./tools/audio.js";
54
+ export * from "./tools/image-prompt.js";
55
+ export { gameEvents } from "./events/emitter.js";
56
+ export type { GameEvent } from "./events/emitter.js";
57
+ export { ANNOTATIONS, withAnnotations } from "./utils/tool-annotations.js";
58
+ export { LIMITS, validatedSchemas, boundedString, boundedArray } from "./utils/validation.js";
59
+ export { createError, formatErrorResponse, errors } from "./utils/errors.js";
60
+ export type { AgentError } from "./utils/errors.js";
61
+ export { createLogger } from "./utils/logger.js";
62
+ export type { Logger } from "./utils/logger.js";
63
+ export { verbositySchema, applyVerbosity, filterFields } from "./utils/verbosity.js";
64
+ export type { VerbosityLevel } from "./utils/verbosity.js";
65
+ export { safeJsonParse, safeJsonParseOrNull } from "./utils/json.js";
66
+ export { successResponseSchema, textResultSchema, deletedResponseSchema, listResponseSchema, characterOutputSchema, characterStatusSchema, conditionModifyOutputSchema, tagModifyOutputSchema, } from "./utils/output-schemas.js";
67
+ export { imageGenSchema, voiceSchema } from "./schemas/index.js";
36
68
  export type * from "./types/index.js";
package/dist/index.js CHANGED
@@ -26,7 +26,13 @@
26
26
  // depending up into it. `src/__tests__/layerBoundary.test.ts` walks the
27
27
  // static import graph from this file and fails if anything under `src/rpg/`
28
28
  // is reachable from it.
29
- export { createCoreMcpServer, SERVER_NAME, SERVER_VERSION } from "./mcp-server.js";
29
+ // The assembled core server is NOT here. It moved to "run-dmcp/server"
30
+ // (src/server.ts) so that importing mechanism stops loading it: building a
31
+ // server costs the MCP SDK and twenty-one register modules, and a consumer
32
+ // reaching for `LIMITS` or `createGame` was paying for both -- 97.4ms per
33
+ // process against 46.8ms without it, cold, at 0.3.0. Enforced by
34
+ // src/__tests__/assemblyBoundary.test.ts, which walks this file's runtime
35
+ // import graph and fails if the assembly is reachable from it again.
30
36
  // The database, and where it lives. The path resolves against the consuming
31
37
  // application (DMCP_DB_PATH, else an existing XDG data directory, else the
32
38
  // working directory) and never against this package's install location.
@@ -181,4 +187,82 @@ export { ENTITY_KINDS } from "./timeline/kinds.js";
181
187
  // ability and combat tools and is exported from "run-dmcp/rpg" instead
182
188
  // (src/rpg/index.ts), even though the file that implements it stays at
183
189
  // src/http/server.ts on disk.
184
- export { DEFAULT_HTTP_PORT, httpPortFromEnv, webUiEnabled, setHttpPort, getWebUiBaseUrl, } from "./utils/webui.js";
190
+ export { DEFAULT_HTTP_PORT, httpPortFromEnv, webUiEnabled, setHttpPort, getWebUiBaseUrl, getGameUrl, getCharacterUrl, getLocationUrl, } from "./utils/webui.js";
191
+ // ===========================================================================
192
+ // The core's own tool modules, as library functions (design §8, §11 Phase 5).
193
+ //
194
+ // §8's layer table puts factions, relationships-with-history, secrets,
195
+ // resources, locations and items in the CORE, and gives the reason: they are
196
+ // "the client's spine. If these go up into the RPG layer, the client cannot
197
+ // consume the package without dragging the RPG layer with it -- which defeats
198
+ // the split." That reason is only satisfied if a consumer can actually IMPORT
199
+ // them. Until this block, it could not: everything above is the timeline and
200
+ // the database, and the only door to the spine was `createCoreMcpServer` --
201
+ // the whole assembled server, which a consumer would then have to make tool
202
+ // calls into, over a transport, to read its own tables.
203
+ //
204
+ // The layer ABOVE this one already got it right. src/rpg/index.ts ends with
205
+ // six export-stars over its own tool modules, under a comment saying it is
206
+ // using "the same shape core's index.ts uses for the timeline... a consumer
207
+ // that wants to call combat/quest/table/status/ability/dice logic directly,
208
+ // without going through an MCP tool call, can." The core never did
209
+ //
210
+ // (Those six are named here in prose rather than quoted as import lines on
211
+ // purpose: layerBoundary.test.ts walks this file's import graph with a
212
+ // deliberately syntactic scan for `from "<specifier>"`, and it cannot tell a
213
+ // quoted example in a comment from a real edge. That is the guard being
214
+ // conservative rather than clever, which is the right trade -- it fails loud
215
+ // and names the chain. Do not teach it to strip comments; reword instead.)
216
+ // the same for its own tools, which left the OPTIONAL layer more consumable
217
+ // than the thing it is optional on top of. That is an oversight and not a
218
+ // decision: §6's rule is "library functions first, MCP tools second", and the
219
+ // narration-constraint and timeline-export blocks above both invoke it.
220
+ //
221
+ // This changes no behaviour and adds no dependency. Every module below is
222
+ // ALREADY in this file's static import graph, reached through
223
+ // ./mcp-server.js -> ./register/* -> ./tools/*, so src/__tests__/
224
+ // layerBoundary.test.ts walks exactly the same file set before and after --
225
+ // nothing under src/rpg/ becomes reachable, and none of the six tool modules
226
+ // that moved up there is named here.
227
+ export * from "./tools/game.js";
228
+ export * from "./tools/world.js";
229
+ export * from "./tools/character.js";
230
+ export * from "./tools/faction.js";
231
+ export * from "./tools/relationship.js";
232
+ export * from "./tools/resource.js";
233
+ export * from "./tools/constraint.js";
234
+ export * from "./tools/inventory.js";
235
+ export * from "./tools/secrets.js";
236
+ export * from "./tools/narrative.js";
237
+ export * from "./tools/notes.js";
238
+ export * from "./tools/tags.js";
239
+ export * from "./tools/time.js";
240
+ export * from "./tools/timers.js";
241
+ export * from "./tools/rules.js";
242
+ export * from "./tools/pause.js";
243
+ export * from "./tools/display.js";
244
+ export * from "./tools/images.js";
245
+ export * from "./tools/audio.js";
246
+ export * from "./tools/image-prompt.js";
247
+ // The event emitter the tool modules above write to. A consumer's own write
248
+ // paths emit through the same singleton, so its tables and the engine's reach
249
+ // one SSE subscriber rather than two competing ones.
250
+ export { gameEvents } from "./events/emitter.js";
251
+ // ===========================================================================
252
+ // What a consumer needs to register tools OF ITS OWN onto the core server.
253
+ //
254
+ // A client keeps its own MCP surface -- that is the whole point of the split;
255
+ // its mechanics are its own and the engine never learns their names. But a
256
+ // tool it registers should refuse, bound and annotate the way the engine's do,
257
+ // and today a client re-implements these or copies them and lets the copy
258
+ // drift. Named one by one rather than star-exported: these modules contain
259
+ // identifiers like `CREATE` and `UPDATE` that have no business in a package's
260
+ // root namespace.
261
+ export { ANNOTATIONS, withAnnotations } from "./utils/tool-annotations.js";
262
+ export { LIMITS, validatedSchemas, boundedString, boundedArray } from "./utils/validation.js";
263
+ export { createError, formatErrorResponse, errors } from "./utils/errors.js";
264
+ export { createLogger } from "./utils/logger.js";
265
+ export { verbositySchema, applyVerbosity, filterFields } from "./utils/verbosity.js";
266
+ export { safeJsonParse, safeJsonParseOrNull } from "./utils/json.js";
267
+ export { successResponseSchema, textResultSchema, deletedResponseSchema, listResponseSchema, characterOutputSchema, characterStatusSchema, conditionModifyOutputSchema, tagModifyOutputSchema, } from "./utils/output-schemas.js";
268
+ export { imageGenSchema, voiceSchema } from "./schemas/index.js";
@@ -2,7 +2,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { type Mechanic } from "./timeline/resolve.js";
3
3
  import { type RenderVocabulary } from "./timeline/render.js";
4
4
  export declare const SERVER_NAME = "dmcp";
5
- export declare const SERVER_VERSION = "0.2.0";
5
+ export declare const SERVER_VERSION = "0.3.0";
6
6
  /**
7
7
  * Build an MCP server with every CORE tool, resource and prompt this engine
8
8
  * serves -- entities, facts, events, the timeline, and the entity/property
@@ -41,7 +41,7 @@ import { registerRenderTools } from "./register/render.js";
41
41
  import { createResolver } from "./timeline/resolve.js";
42
42
  import { createStateRenderer } from "./timeline/render.js";
43
43
  export const SERVER_NAME = "dmcp";
44
- export const SERVER_VERSION = "0.2.0";
44
+ export const SERVER_VERSION = "0.3.0";
45
45
  /**
46
46
  * Build an MCP server with every CORE tool, resource and prompt this engine
47
47
  * serves -- entities, facts, events, the timeline, and the entity/property
@@ -9,7 +9,7 @@ export function registerResourceTools(server) {
9
9
  description: "Create a new resource (currency, reputation, counter, etc.)",
10
10
  inputSchema: {
11
11
  gameId: z.string().max(100).describe("The game ID"),
12
- ownerType: z.enum(["game", "character"]).describe("Owner type: 'game' for party/global resources, 'character' for personal resources"),
12
+ ownerType: z.enum(["game", "character", "faction", "location"]).describe("Owner type: 'game' for party/global resources, 'character' for personal resources, 'faction' or 'location' for resources owned by one of those entities"),
13
13
  ownerId: z.string().max(100).optional().describe("Character ID if ownerType is 'character' (omit for game-level resources)"),
14
14
  name: z.string().min(1).max(LIMITS.NAME_MAX).describe("Resource name (e.g., 'Gold', 'Sanity', 'Thieves Guild Reputation')"),
15
15
  description: z.string().max(LIMITS.DESCRIPTION_MAX).optional().describe("Resource description"),
@@ -105,7 +105,7 @@ export function registerResourceTools(server) {
105
105
  description: "List resources in a game",
106
106
  inputSchema: {
107
107
  gameId: z.string().max(100).describe("The game ID"),
108
- ownerType: z.enum(["game", "character"]).optional().describe("Filter by owner type"),
108
+ ownerType: z.enum(["game", "character", "faction", "location"]).optional().describe("Filter by owner type"),
109
109
  ownerId: z.string().max(100).optional().describe("Filter by owner ID (for character resources)"),
110
110
  category: z.string().max(100).optional().describe("Filter by category"),
111
111
  },
@@ -1,26 +1,10 @@
1
1
  import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
- import type { Mechanic } from "../timeline/resolve.js";
3
- import type { RenderVocabulary } from "../timeline/render.js";
4
2
  /**
5
3
  * Register every RPG-layer tool, resource and prompt onto an existing
6
4
  * server. Combat brings dice with it (registerCombatTools registers both,
7
5
  * matching how the pre-split assembly registered them as one domain).
8
6
  */
9
7
  export declare function registerRpgTools(server: McpServer): void;
10
- /**
11
- * Build the FULL assembly: every core tool plus every RPG tool, resource and
12
- * prompt this engine has always served. Same name, same `{ mechanics?,
13
- * vocabulary? }` options as the pre-split `createMcpServer` in
14
- * src/mcp-server.ts -- this is that function, now composed from two layers
15
- * instead of one, so src/bin/run-dmcp.ts's zero-argument call site keeps
16
- * working unchanged and the registered tool/resource/prompt set is
17
- * unchanged too (src/__tests__/layerBoundary.test.ts asserts this exactly).
18
- */
19
- export declare function createMcpServer(options?: {
20
- mechanics?: readonly Mechanic[];
21
- vocabulary?: RenderVocabulary;
22
- }): McpServer;
23
- export { createHttpServer, startHttpServer } from "../http/server.js";
24
8
  export * from "./tools/dice.js";
25
9
  export * from "./tools/combat.js";
26
10
  export * from "./tools/ability.js";
package/dist/rpg/index.js CHANGED
@@ -1,4 +1,3 @@
1
- import { createCoreMcpServer } from "../mcp-server.js";
2
1
  import { registerCombatTools } from "./register/combat.js";
3
2
  import { registerAbilityTools } from "./register/abilities.js";
4
3
  import { registerStatusTools } from "./register/status.js";
@@ -22,27 +21,10 @@ export function registerRpgTools(server) {
22
21
  registerRpgMcpResources(server); // game-quests, quest
23
22
  registerMcpPrompts(server); // Reusable prompt templates (game-master session library)
24
23
  }
25
- /**
26
- * Build the FULL assembly: every core tool plus every RPG tool, resource and
27
- * prompt this engine has always served. Same name, same `{ mechanics?,
28
- * vocabulary? }` options as the pre-split `createMcpServer` in
29
- * src/mcp-server.ts -- this is that function, now composed from two layers
30
- * instead of one, so src/bin/run-dmcp.ts's zero-argument call site keeps
31
- * working unchanged and the registered tool/resource/prompt set is
32
- * unchanged too (src/__tests__/layerBoundary.test.ts asserts this exactly).
33
- */
34
- export function createMcpServer(options) {
35
- const server = createCoreMcpServer(options);
36
- registerRpgTools(server);
37
- return server;
38
- }
39
- // The web UI. An application opts into serving it; importing this never
40
- // does. It lives on disk at src/http/server.ts, not under src/rpg/ -- it
41
- // imports RPG tools (quest/ability/combat), which is why it cannot be
42
- // reachable from the core entry point, but its CLIENT_DIST path resolution
43
- // is depth-sensitive in both src/ and compiled dist/, so it stays put and
44
- // only its EXPORT moves up to this layer.
45
- export { createHttpServer, startHttpServer } from "../http/server.js";
24
+ // `createMcpServer` and the web UI are NOT here. Both are assembly, and both
25
+ // moved to "run-dmcp/rpg/server" (src/rpg/server.ts) so that importing the
26
+ // layer's tool functions stops loading a server and an HTTP framework with
27
+ // them.
46
28
  // The RPG tool modules, exported as library functions -- the same shape
47
29
  // core's index.ts uses for the timeline (replay, changesWithin, and so on):
48
30
  // a consumer that wants to call combat/quest/table/status/ability/dice logic
@@ -0,0 +1,16 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { Mechanic } from "../timeline/resolve.js";
3
+ import type { RenderVocabulary } from "../timeline/render.js";
4
+ /**
5
+ * Build the FULL assembly: every core tool plus every RPG tool, resource and
6
+ * prompt this engine has always served. Same name and same `{ mechanics?,
7
+ * vocabulary? }` options as the pre-split `createMcpServer` in
8
+ * src/mcp-server.ts, so src/bin/run-dmcp.ts's zero-argument call site keeps
9
+ * working unchanged and the registered tool/resource/prompt set is unchanged
10
+ * too (src/__tests__/layerBoundary.test.ts asserts this exactly).
11
+ */
12
+ export declare function createMcpServer(options?: {
13
+ mechanics?: readonly Mechanic[];
14
+ vocabulary?: RenderVocabulary;
15
+ }): McpServer;
16
+ export { createHttpServer, startHttpServer } from "../http/server.js";