run-dmcp 0.4.0 → 0.6.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.
- package/dist/mcp-server.d.ts +5 -1
- package/dist/mcp-server.js +5 -1
- package/dist/register/batch.js +11 -10
- package/dist/register/character.js +16 -16
- package/dist/register/core.js +128 -111
- package/dist/register/display.js +42 -41
- package/dist/register/resolve.js +4 -1
- package/dist/register/world.js +13 -13
- package/dist/rpg/register/batch.js +3 -3
- package/dist/schemas/index.js +91 -72
- package/dist/timeline/export.d.ts +13 -4
- package/dist/timeline/export.js +15 -9
- package/dist/timeline/irreversible.js +11 -6
- package/dist/timeline/narration.js +6 -4
- package/dist/timeline/projection.js +21 -4
- package/dist/timeline/provenance.d.ts +21 -28
- package/dist/timeline/provenance.js +30 -34
- package/dist/timeline/schema.js +29 -1
- package/dist/utils/validation.d.ts +31 -2
- package/dist/utils/validation.js +67 -10
- package/package.json +1 -1
|
@@ -1,45 +1,41 @@
|
|
|
1
1
|
import { getDatabase } from "../db/connection.js";
|
|
2
2
|
/**
|
|
3
|
-
* The one hop of causality (design §5.2c)
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
3
|
+
* The one hop of causality (design §5.2c), READ rather than derived (issue
|
|
4
|
+
* #30). `facts.opened_by_event_id` is stamped by the projection triggers'
|
|
5
|
+
* `_ai`/`_au` bodies (projection.ts) at the moment a fact opens, in the same
|
|
6
|
+
* firing, via `last_insert_rowid()` against the event they just inserted --
|
|
7
|
+
* so this is a direct column lookup now, not a search over `events` keyed by
|
|
8
|
+
* `(at_t, causes.row_id)` with a random-hex tiebreak among rows sharing a
|
|
9
|
+
* `t`. That derivation is gone, along with the failure modes it carried: it
|
|
10
|
+
* could return null for an event whose `causes` was not valid JSON, and it
|
|
11
|
+
* broke ties among same-`t` events arbitrarily.
|
|
12
12
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* query, not to the offending row, so a single bad row anywhere in this
|
|
18
|
-
* game's events would make every function that calls this throw, including
|
|
19
|
-
* ones that have nothing to do with that event. That is reachable in
|
|
20
|
-
* practice: timeline import (export.ts) carries `causes` through verbatim
|
|
21
|
-
* by design, because an importer that rewrote a recorded cause would be
|
|
22
|
-
* inventing history. A hop of provenance must never be able to fail the
|
|
23
|
-
* write it annotates, so a row we cannot read simply does not match.
|
|
24
|
-
* Written as CASE rather than `json_valid(causes) AND json_extract(...)`
|
|
25
|
-
* because SQLite does not guarantee the evaluation order of AND operands --
|
|
26
|
-
* the planner may reorder them, and then the guard is decoration that
|
|
27
|
-
* happens to work today.
|
|
13
|
+
* Both internal callers of this shape (`irreversible.ts`, `narration.ts`)
|
|
14
|
+
* no longer call this function at all -- each already queries its own fact
|
|
15
|
+
* row and now selects `opened_by_event_id` directly as part of that same
|
|
16
|
+
* query, which is strictly cheaper than a second round trip through here.
|
|
28
17
|
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
18
|
+
* This function is kept, and re-pointed at the stored column rather than
|
|
19
|
+
* removed, because it is part of this package's published library surface
|
|
20
|
+
* (`src/index.ts` re-exports it) -- issue #30 did not ask for a public API
|
|
21
|
+
* removal, and removing an exported function silently would be exactly the
|
|
22
|
+
* kind of undocumented break root CLAUDE.md's "what we declare is what we
|
|
23
|
+
* mean" section warns against. A caller that already holds
|
|
24
|
+
* `(gameId, entityId, validFromT)` rather than a fact id can still use it;
|
|
25
|
+
* it now does less work to answer the same question.
|
|
33
26
|
*/
|
|
34
27
|
export function openingEventId(gameId, entityId, validFromT) {
|
|
35
28
|
const db = getDatabase();
|
|
36
29
|
const row = db
|
|
37
|
-
.prepare(`SELECT
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
30
|
+
.prepare(`SELECT f.opened_by_event_id AS id
|
|
31
|
+
FROM facts f
|
|
32
|
+
JOIN entities e ON e.id = f.entity_id
|
|
33
|
+
WHERE e.game_id = ?
|
|
34
|
+
AND f.entity_id = ?
|
|
35
|
+
AND f.valid_from_t = ?
|
|
36
|
+
AND f.opened_by_event_id IS NOT NULL
|
|
37
|
+
ORDER BY f.id
|
|
42
38
|
LIMIT 1`)
|
|
43
|
-
.get(gameId,
|
|
39
|
+
.get(gameId, entityId, validFromT);
|
|
44
40
|
return row?.id ?? null;
|
|
45
41
|
}
|
package/dist/timeline/schema.js
CHANGED
|
@@ -84,6 +84,23 @@ export function initializeTimelineSchema() {
|
|
|
84
84
|
causes TEXT
|
|
85
85
|
)
|
|
86
86
|
`);
|
|
87
|
+
// facts.opened_by_event_id -- issue #30: the one hop of causality (design
|
|
88
|
+
// §5.2c) recorded at the moment it is true, instead of derived at read
|
|
89
|
+
// time by searching `events` for a row whose `at_t`/`causes.row_id` happen
|
|
90
|
+
// to match (the query this replaces lived in provenance.ts, and could
|
|
91
|
+
// return null for a bad `causes` JSON blob, or pick the wrong event of
|
|
92
|
+
// several sharing one `t` by an arbitrary hex-id tiebreak). Idempotent
|
|
93
|
+
// ALTER, ordered after `events` exists -- legal under SQLite's ADD COLUMN
|
|
94
|
+
// restrictions because the implicit default is NULL, and a database that
|
|
95
|
+
// already has this column (every run after the first) just throws here,
|
|
96
|
+
// caught and ignored, same idiom as every other migration in this
|
|
97
|
+
// codebase (root CLAUDE.md).
|
|
98
|
+
try {
|
|
99
|
+
db.exec("ALTER TABLE facts ADD COLUMN opened_by_event_id TEXT REFERENCES events(id)");
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
// Column already exists.
|
|
103
|
+
}
|
|
87
104
|
// timeline_clock -- one row per game, tracking the declared axis and its
|
|
88
105
|
// current position. A game that never declares an axis still needs one
|
|
89
106
|
// of these once its first entity/fact/event is written, with axis_kind
|
|
@@ -152,6 +169,16 @@ export function initializeTimelineSchema() {
|
|
|
152
169
|
// change. declareIrreversible() (irreversible.ts) relies on that last
|
|
153
170
|
// case to make re-declaring idempotent with a single UPDATE and no
|
|
154
171
|
// separate "already set" check.
|
|
172
|
+
// Third one-way latch (issue #30): NULL -> value, once, exactly like
|
|
173
|
+
// `valid_to_t` above -- once the projection trigger has stamped
|
|
174
|
+
// `opened_by_event_id`, nothing may rewrite it, but the stamp itself (the
|
|
175
|
+
// trigger's own UPDATE, immediately after the fact's INSERT -- see
|
|
176
|
+
// projection.ts) must be allowed through. This clause is load-bearing, not
|
|
177
|
+
// hygiene: probed against better-sqlite3 at this project's own pragma
|
|
178
|
+
// settings, the append-only guard fires on trigger-initiated UPDATEs too.
|
|
179
|
+
// Without it, the projection trigger's own stamp would abort the write it
|
|
180
|
+
// annotates; with it, a stamped edge can never be rewritten -- a re-stamp
|
|
181
|
+
// raises ABORT.
|
|
155
182
|
db.exec(`
|
|
156
183
|
DROP TRIGGER IF EXISTS timeline_facts_immutable;
|
|
157
184
|
CREATE TRIGGER timeline_facts_immutable
|
|
@@ -164,8 +191,9 @@ export function initializeTimelineSchema() {
|
|
|
164
191
|
OR (NEW.irreversible IS NOT OLD.irreversible
|
|
165
192
|
AND (OLD.irreversible IS NOT 0 OR NEW.irreversible IS NOT 1))
|
|
166
193
|
OR (OLD.valid_to_t IS NOT NULL AND NEW.valid_to_t IS NOT OLD.valid_to_t)
|
|
194
|
+
OR (OLD.opened_by_event_id IS NOT NULL AND NEW.opened_by_event_id IS NOT OLD.opened_by_event_id)
|
|
167
195
|
BEGIN
|
|
168
|
-
SELECT RAISE(ABORT, 'timeline: facts are append-only; valid_from_t cannot be rewritten, valid_to_t may only be closed once,
|
|
196
|
+
SELECT RAISE(ABORT, 'timeline: facts are append-only; valid_from_t cannot be rewritten, valid_to_t may only be closed once, irreversible may only move 0 -> 1, and opened_by_event_id may only be stamped once');
|
|
169
197
|
END;
|
|
170
198
|
`);
|
|
171
199
|
db.exec(`
|
|
@@ -1,24 +1,53 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
/**
|
|
3
3
|
* Common input length limits for security and performance.
|
|
4
|
-
*
|
|
4
|
+
*
|
|
5
|
+
* These prevent DoS attacks via extremely large inputs -- and they are also
|
|
6
|
+
* the engine's PUBLISHED CONTRACT. A `.max()` here lands in the tool's
|
|
7
|
+
* `input_schema` and `tools/list` hands it to every client before the first
|
|
8
|
+
* call, so a consumer can read what a field takes instead of hard-coding a
|
|
9
|
+
* copy of these numbers. One does. Leaving a string parameter unbounded is
|
|
10
|
+
* therefore not a missing safety net but an active statement that the field
|
|
11
|
+
* takes anything; `src/__tests__/schemaBounds.test.ts` fails on any that is
|
|
12
|
+
* (issue #29).
|
|
5
13
|
*/
|
|
6
14
|
export declare const LIMITS: {
|
|
7
15
|
readonly NAME_MAX: 200;
|
|
8
16
|
readonly DESCRIPTION_MAX: 5000;
|
|
9
17
|
readonly CONTENT_MAX: 50000;
|
|
10
18
|
readonly NARRATIVE_MAX: 200000;
|
|
19
|
+
readonly EMBEDDED_DATA_MAX: 2000000;
|
|
11
20
|
readonly ARRAY_MAX: 100;
|
|
12
21
|
readonly MAX_DEPTH: 10;
|
|
13
22
|
};
|
|
14
23
|
/**
|
|
15
|
-
* Pre-built Zod schemas with length limits
|
|
24
|
+
* Pre-built Zod schemas with length limits.
|
|
25
|
+
*
|
|
26
|
+
* EVERY PROPERTY HERE IS A GETTER, AND HANDS BACK A FRESH INSTANCE.
|
|
27
|
+
*
|
|
28
|
+
* That is not a style: `zod-to-json-schema` (which the MCP SDK runs over
|
|
29
|
+
* every `inputSchema` on its way to `tools/list`) de-duplicates by object
|
|
30
|
+
* IDENTITY. Reuse one instance twice inside a single tool and the second
|
|
31
|
+
* occurrence is published as
|
|
32
|
+
*
|
|
33
|
+
* { "$ref": "#/properties/imageGen/properties/subject/properties/..." }
|
|
34
|
+
*
|
|
35
|
+
* -- a pointer into a sibling property, in place of the declaration a
|
|
36
|
+
* consumer came to read. Sharing these as plain constants put 648 of those
|
|
37
|
+
* into the contract, where there had been none; a fresh instance per access
|
|
38
|
+
* puts the declaration back at every site. `src/__tests__/schemaBounds.test.ts`
|
|
39
|
+
* fails on any `$ref` in a published schema, which is what caught it.
|
|
40
|
+
*
|
|
41
|
+
* The cost is a schema object built per access. They are read at server
|
|
42
|
+
* construction, not per call.
|
|
16
43
|
*/
|
|
17
44
|
export declare const validatedSchemas: {
|
|
18
45
|
readonly name: z.ZodString;
|
|
46
|
+
readonly token: z.ZodString;
|
|
19
47
|
readonly description: z.ZodString;
|
|
20
48
|
readonly content: z.ZodString;
|
|
21
49
|
readonly narrative: z.ZodString;
|
|
50
|
+
readonly embeddedData: z.ZodString;
|
|
22
51
|
readonly id: z.ZodString;
|
|
23
52
|
readonly tag: z.ZodString;
|
|
24
53
|
readonly stringArray: z.ZodArray<z.ZodString, "many">;
|
package/dist/utils/validation.js
CHANGED
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
/**
|
|
3
3
|
* Common input length limits for security and performance.
|
|
4
|
-
*
|
|
4
|
+
*
|
|
5
|
+
* These prevent DoS attacks via extremely large inputs -- and they are also
|
|
6
|
+
* the engine's PUBLISHED CONTRACT. A `.max()` here lands in the tool's
|
|
7
|
+
* `input_schema` and `tools/list` hands it to every client before the first
|
|
8
|
+
* call, so a consumer can read what a field takes instead of hard-coding a
|
|
9
|
+
* copy of these numbers. One does. Leaving a string parameter unbounded is
|
|
10
|
+
* therefore not a missing safety net but an active statement that the field
|
|
11
|
+
* takes anything; `src/__tests__/schemaBounds.test.ts` fails on any that is
|
|
12
|
+
* (issue #29).
|
|
5
13
|
*/
|
|
6
14
|
export const LIMITS = {
|
|
7
15
|
// Short text fields (names, titles)
|
|
@@ -12,31 +20,80 @@ export const LIMITS = {
|
|
|
12
20
|
CONTENT_MAX: 50000,
|
|
13
21
|
// Very long text fields (story exports, narrative)
|
|
14
22
|
NARRATIVE_MAX: 200000,
|
|
23
|
+
// Inline binary carried as a data URI or bare base64 -- an image, not prose.
|
|
24
|
+
// Deliberately generous rather than a prose tier: the fields that take one
|
|
25
|
+
// (`imageGen.generations[].base64`) accepted anything before they were
|
|
26
|
+
// bounded, and a real inline PNG is hundreds of kilobytes.
|
|
27
|
+
EMBEDDED_DATA_MAX: 2000000,
|
|
15
28
|
// Array limits
|
|
16
29
|
ARRAY_MAX: 100,
|
|
17
30
|
// JSON object depth limit (for nested structures)
|
|
18
31
|
MAX_DEPTH: 10,
|
|
19
32
|
};
|
|
20
33
|
/**
|
|
21
|
-
* Pre-built Zod schemas with length limits
|
|
34
|
+
* Pre-built Zod schemas with length limits.
|
|
35
|
+
*
|
|
36
|
+
* EVERY PROPERTY HERE IS A GETTER, AND HANDS BACK A FRESH INSTANCE.
|
|
37
|
+
*
|
|
38
|
+
* That is not a style: `zod-to-json-schema` (which the MCP SDK runs over
|
|
39
|
+
* every `inputSchema` on its way to `tools/list`) de-duplicates by object
|
|
40
|
+
* IDENTITY. Reuse one instance twice inside a single tool and the second
|
|
41
|
+
* occurrence is published as
|
|
42
|
+
*
|
|
43
|
+
* { "$ref": "#/properties/imageGen/properties/subject/properties/..." }
|
|
44
|
+
*
|
|
45
|
+
* -- a pointer into a sibling property, in place of the declaration a
|
|
46
|
+
* consumer came to read. Sharing these as plain constants put 648 of those
|
|
47
|
+
* into the contract, where there had been none; a fresh instance per access
|
|
48
|
+
* puts the declaration back at every site. `src/__tests__/schemaBounds.test.ts`
|
|
49
|
+
* fails on any `$ref` in a published schema, which is what caught it.
|
|
50
|
+
*
|
|
51
|
+
* The cost is a schema object built per access. They are read at server
|
|
52
|
+
* construction, not per call.
|
|
22
53
|
*/
|
|
23
54
|
export const validatedSchemas = {
|
|
24
55
|
// Name fields (character names, location names, etc.)
|
|
25
|
-
name
|
|
56
|
+
get name() {
|
|
57
|
+
return z.string().min(1).max(LIMITS.NAME_MAX);
|
|
58
|
+
},
|
|
59
|
+
// Short free text with a ceiling and NO floor -- a colour, a direction, an
|
|
60
|
+
// aspect ratio, a sampler. Same length as `name`, without `.min(1)`: bounding
|
|
61
|
+
// a field that accepts "" today must not also start rejecting it.
|
|
62
|
+
get token() {
|
|
63
|
+
return z.string().max(LIMITS.NAME_MAX);
|
|
64
|
+
},
|
|
26
65
|
// Description fields
|
|
27
|
-
description
|
|
66
|
+
get description() {
|
|
67
|
+
return z.string().max(LIMITS.DESCRIPTION_MAX);
|
|
68
|
+
},
|
|
28
69
|
// Content fields (notes, large text)
|
|
29
|
-
content
|
|
70
|
+
get content() {
|
|
71
|
+
return z.string().max(LIMITS.CONTENT_MAX);
|
|
72
|
+
},
|
|
30
73
|
// Narrative content (very large text allowed)
|
|
31
|
-
narrative
|
|
74
|
+
get narrative() {
|
|
75
|
+
return z.string().max(LIMITS.NARRATIVE_MAX);
|
|
76
|
+
},
|
|
77
|
+
// Inline binary carried as base64 or a data URI -- an image, not prose.
|
|
78
|
+
get embeddedData() {
|
|
79
|
+
return z.string().max(LIMITS.EMBEDDED_DATA_MAX);
|
|
80
|
+
},
|
|
32
81
|
// ID fields (UUIDs are 36 characters)
|
|
33
|
-
id
|
|
82
|
+
get id() {
|
|
83
|
+
return z.string().max(100);
|
|
84
|
+
},
|
|
34
85
|
// Tag/category strings
|
|
35
|
-
tag
|
|
86
|
+
get tag() {
|
|
87
|
+
return z.string().min(1).max(100);
|
|
88
|
+
},
|
|
36
89
|
// Array of strings with limits
|
|
37
|
-
stringArray
|
|
90
|
+
get stringArray() {
|
|
91
|
+
return z.array(z.string().max(LIMITS.NAME_MAX)).max(LIMITS.ARRAY_MAX);
|
|
92
|
+
},
|
|
38
93
|
// Array of tags
|
|
39
|
-
tagArray
|
|
94
|
+
get tagArray() {
|
|
95
|
+
return z.array(z.string().min(1).max(100)).max(LIMITS.ARRAY_MAX);
|
|
96
|
+
},
|
|
40
97
|
};
|
|
41
98
|
/**
|
|
42
99
|
* Helper to create a bounded string schema
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "run-dmcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "An MCP server for LLM-run interactive fiction where the server owns what is true - including when it was true. A continuation of DMCP.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Derek Ferguson",
|