bantamkit-mcp 0.26.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/LICENSE +21 -0
- package/README.md +398 -0
- package/assets/contracts/default.yaml +90 -0
- package/assets/evals/devteam/manifest.yaml +351 -0
- package/assets/evals/devteam/repo/HISTORY.md +18 -0
- package/assets/evals/devteam/repo/README.md +12 -0
- package/assets/evals/devteam/repo/docs/architecture.md +17 -0
- package/assets/evals/devteam/repo/docs/runbook.md +10 -0
- package/assets/evals/devteam/repo/issues/142-settlement-timeout.md +23 -0
- package/assets/evals/devteam/repo/patches/0009-retry-budget.patch +38 -0
- package/assets/evals/devteam/repo/src/ledger/__init__.py +3 -0
- package/assets/evals/devteam/repo/src/ledger/config.py +35 -0
- package/assets/evals/devteam/repo/src/ledger/errors.py +13 -0
- package/assets/evals/devteam/repo/src/ledger/posting.py +12 -0
- package/assets/evals/devteam/repo/src/ledger/registry.py +7 -0
- package/assets/evals/devteam/repo/src/ledger/report.py +9 -0
- package/assets/evals/devteam/repo/src/ledger/retry.py +17 -0
- package/assets/evals/devteam/repo/src/ledger/settle.py +16 -0
- package/assets/evals/devteam/repo/src/ledger/validate.py +14 -0
- package/assets/evals/devteam/repo/tests/test_posting.py +13 -0
- package/assets/evals/devteam/repo/tests/test_settle.py +9 -0
- package/assets/evals/devteam/tasks/dt-error-contract.yaml +186 -0
- package/assets/evals/devteam/tasks/dt-handler-map.yaml +183 -0
- package/assets/evals/devteam/tasks/dt-patch-before-after.yaml +182 -0
- package/assets/evals/devteam/tasks/dt-retry-attempts.yaml +181 -0
- package/assets/evals/devteam/tasks/dt-settlement-config.yaml +185 -0
- package/assets/evals/devteam/tasks/dt-symbol-home.yaml +181 -0
- package/assets/evals/devteam/tasks/dt-trace-blame.yaml +182 -0
- package/assets/evals/devteam/tasks/dt-unread-key.yaml +180 -0
- package/assets/evals/document/tasks/doc-large-in-137.yaml +38 -0
- package/assets/evals/document/tasks/doc-large-in-359.yaml +44 -0
- package/assets/evals/document/tasks/doc-large-in-372.yaml +38 -0
- package/assets/evals/document/tasks/doc-large-out-11764.yaml +37 -0
- package/assets/evals/document/tasks/doc-large-out-4137.yaml +37 -0
- package/assets/evals/document/tasks/doc-large-out-8022.yaml +37 -0
- package/assets/evals/document/tasks/doc-small-137.yaml +37 -0
- package/assets/evals/document/tasks/doc-small-261.yaml +37 -0
- package/assets/evals/document/tasks/doc-small-388.yaml +37 -0
- package/assets/evals/fixtures/.gitkeep +0 -0
- package/assets/evals/fixtures/catalog.json +6 -0
- package/assets/evals/perturbations/task-completion.yaml +576 -0
- package/assets/evals/tasks/.gitkeep +0 -0
- package/assets/evals/tasks/extract-contact.yaml +14 -0
- package/assets/evals/tasks/extract-invoice.yaml +14 -0
- package/assets/evals/tasks/extract-order.yaml +15 -0
- package/assets/evals/tasks/extract-schedule.yaml +14 -0
- package/assets/evals/tasks/extract-versions.yaml +17 -0
- package/assets/evals/tasks/nav-prod-port.yaml +84 -0
- package/assets/evals/tasks/nav-release-bundle.yaml +87 -0
- package/assets/evals/tasks/recall-audit-retention.yaml +17 -0
- package/assets/evals/tasks/recall-cache-ttl.yaml +13 -0
- package/assets/evals/tasks/recall-db-port.yaml +17 -0
- package/assets/evals/tasks/recall-deploy.yaml +13 -0
- package/assets/evals/tasks/recall-env-endpoint.yaml +18 -0
- package/assets/evals/tasks/recall-oncall-rotation.yaml +21 -0
- package/assets/evals/tasks/recall-oncall.yaml +13 -0
- package/assets/evals/tasks/recall-org-quota.yaml +18 -0
- package/assets/evals/tasks/recall-owner.yaml +13 -0
- package/assets/evals/tasks/shop-basket-total.yaml +10 -0
- package/assets/evals/tasks/shop-cheapest.yaml +9 -0
- package/assets/evals/tasks/shop-compare.yaml +9 -0
- package/assets/evals/tasks/shop-gadget-value.yaml +9 -0
- package/assets/evals/tasks/shop-stock-total.yaml +9 -0
- package/assets/evals/tasks/shop-total.yaml +9 -0
- package/assets/profiles/default.yaml +31 -0
- package/assets/profiles/patient.yaml +31 -0
- package/assets/rubrics/.gitkeep +0 -0
- package/assets/rubrics/code-quality.yaml +20 -0
- package/assets/rubrics/grounded-completion.yaml +37 -0
- package/assets/rubrics/task-completion.yaml +28 -0
- package/assets/schemas/shiftwork-checkpoint.json +188 -0
- package/assets/skills/.gitkeep +0 -0
- package/assets/skills/file-graph.md +7 -0
- package/assets/skills/memory.md +35 -0
- package/assets/tools/.gitkeep +0 -0
- package/assets/tools/bantamkit_read.json +48 -0
- package/assets/tools/bantamkit_status.json +25 -0
- package/assets/tools/build_identity.json +17 -0
- package/assets/tools/document_list.json +12 -0
- package/assets/tools/document_read.json +31 -0
- package/assets/tools/file_graph.json +12 -0
- package/assets/tools/memory_compact.json +31 -0
- package/assets/tools/memory_recall.json +38 -0
- package/assets/tools/memory_save.json +61 -0
- package/assets/tools/shiftwork_clock_in.json +25 -0
- package/assets/tools/shiftwork_clock_out.json +60 -0
- package/assets/tools/shiftwork_status.json +25 -0
- package/assets/tools/skill_audit.json +70 -0
- package/assets/tools/validate_json.json +31 -0
- package/dist/assets.d.ts +48 -0
- package/dist/assets.js +109 -0
- package/dist/charsets.d.ts +17 -0
- package/dist/charsets.js +580 -0
- package/dist/cli.d.ts +12 -0
- package/dist/cli.js +280 -0
- package/dist/contract.d.ts +145 -0
- package/dist/contract.js +608 -0
- package/dist/docread.d.ts +357 -0
- package/dist/docread.js +3315 -0
- package/dist/errors.d.ts +11 -0
- package/dist/errors.js +14 -0
- package/dist/eventlog.d.ts +137 -0
- package/dist/eventlog.js +294 -0
- package/dist/htmlentities.d.ts +2 -0
- package/dist/htmlentities.js +16 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +13 -0
- package/dist/mcp/identity.d.ts +25 -0
- package/dist/mcp/identity.js +275 -0
- package/dist/mcp/pyargs.d.ts +80 -0
- package/dist/mcp/pyargs.js +362 -0
- package/dist/mcp/sdkjson.d.ts +56 -0
- package/dist/mcp/sdkjson.js +91 -0
- package/dist/mcp/server.d.ts +45 -0
- package/dist/mcp/server.js +740 -0
- package/dist/mcp/status.d.ts +210 -0
- package/dist/mcp/status.js +357 -0
- package/dist/mcp/transport.d.ts +68 -0
- package/dist/mcp/transport.js +111 -0
- package/dist/mcpreport.d.ts +214 -0
- package/dist/mcpreport.js +758 -0
- package/dist/memory/cli.d.ts +2 -0
- package/dist/memory/cli.js +396 -0
- package/dist/memory/component.d.ts +256 -0
- package/dist/memory/component.js +497 -0
- package/dist/memory/factfile.d.ts +120 -0
- package/dist/memory/factfile.js +406 -0
- package/dist/memory/layers.d.ts +92 -0
- package/dist/memory/layers.js +410 -0
- package/dist/memory/pyfs.d.ts +521 -0
- package/dist/memory/pyfs.js +1608 -0
- package/dist/memory/pyyaml.d.ts +155 -0
- package/dist/memory/pyyaml.js +868 -0
- package/dist/memory/store.d.ts +478 -0
- package/dist/memory/store.js +1034 -0
- package/dist/pyargparse.d.ts +141 -0
- package/dist/pyargparse.js +813 -0
- package/dist/pyjson.d.ts +110 -0
- package/dist/pyjson.js +524 -0
- package/dist/pyjsonschema.d.ts +50 -0
- package/dist/pyjsonschema.js +951 -0
- package/dist/shiftwork.d.ts +60 -0
- package/dist/shiftwork.js +324 -0
- package/dist/skillaudit.d.ts +172 -0
- package/dist/skillaudit.js +914 -0
- package/dist/statusline.d.ts +106 -0
- package/dist/statusline.js +281 -0
- package/package.json +45 -0
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Argument validation, in the reference runtime's own words.
|
|
3
|
+
*
|
|
4
|
+
* WHY THIS IS REPRODUCED RATHER THAN RULED. The text below is not a diagnostic in a log —
|
|
5
|
+
* it is the `content[0].text` of a tool result, which means the MODEL reads it and decides
|
|
6
|
+
* what to do next. `zod`'s wording is entirely different, so a port that let zod speak would
|
|
7
|
+
* ship a server whose recovery behaviour differs from the reference's on every mistyped
|
|
8
|
+
* argument, and that is a product difference, not a cosmetic one.
|
|
9
|
+
*
|
|
10
|
+
* WHAT THE URL IS. `https://errors.pydantic.dev/2.13/v/<type>` names an entry in pydantic's
|
|
11
|
+
* public error catalogue. This runtime has no pydantic; the URL is part of the sentence the
|
|
12
|
+
* reference emits, reproduced because the sentence is the contract. `2.13` is therefore a
|
|
13
|
+
* PIN on the reference, not a claim about this process — and it is compared against the
|
|
14
|
+
* live server by `tools/conformance/suites/wire.mjs`, so the day the Python side bumps to
|
|
15
|
+
* 2.14 the suite goes red here instead of the two runtimes drifting apart quietly.
|
|
16
|
+
*
|
|
17
|
+
* EVERY RULE BELOW WAS MEASURED, not read off pydantic's documentation. The probes are in
|
|
18
|
+
* `scratchpad/job38-n7/probe{4,7,8,9}.py` and each drove the real stdio server:
|
|
19
|
+
*
|
|
20
|
+
* * errors come in MODEL FIELD order, not in the order the arguments arrived (probe7 id 80
|
|
21
|
+
* sends `body, name, description, type` and gets `type` before `name`);
|
|
22
|
+
* * `int` is LAX — `True`, `'2'`, `' 3 '`, `'2_0'`, `'+2'` and `3.0` all validate, while
|
|
23
|
+
* `'2.5'`, `'1e3'`, `'0x10'` and `''` are `int_parsing` and `2.5` is `int_from_float`;
|
|
24
|
+
* * a float too large for an i64 is `int_parsing_size`, with a DIFFERENT message shape
|
|
25
|
+
* (no "Input should be a valid integer," prefix);
|
|
26
|
+
* * `input_value` is truncated on the UTF-8 BYTE length of `repr(value)`: over 50 bytes it
|
|
27
|
+
* becomes head-25-bytes + `...` + tail-24-bytes, each end floored to a character
|
|
28
|
+
* boundary. Thirty Thai characters are 34 repr CHARACTERS and are still truncated.
|
|
29
|
+
*/
|
|
30
|
+
import { reprValue } from '../pyjson.js';
|
|
31
|
+
const req = (name, kind) => ({ name, kind, optional: false });
|
|
32
|
+
const opt = (name, kind, bound = {}) => ({
|
|
33
|
+
name,
|
|
34
|
+
kind,
|
|
35
|
+
optional: true,
|
|
36
|
+
...bound,
|
|
37
|
+
});
|
|
38
|
+
/**
|
|
39
|
+
* `assets/tools/bantamkit_read.json` `offset.maximum` — `Number.MAX_SAFE_INTEGER`, the
|
|
40
|
+
* largest integer `JSON.parse` reads back unchanged (F1). The reference binds it as
|
|
41
|
+
* `Annotated[int, Field(le=OFFSET_MAXIMUM)]`, so above it the refusal is pydantic's
|
|
42
|
+
* `less_than_equal` frame and the handler never runs; this side reads the JSON with its
|
|
43
|
+
* own decoder, so `9007199254740993` arrives exact and is refused with the same text.
|
|
44
|
+
*/
|
|
45
|
+
const OFFSET_MAXIMUM = 9007199254740991n;
|
|
46
|
+
/**
|
|
47
|
+
* One model per handler, mirroring `build_server`'s closures signature for signature.
|
|
48
|
+
*
|
|
49
|
+
* `build_identity`'s model is `build_identity_toolArguments` and not `build_identityArguments`
|
|
50
|
+
* — pydantic names it after the PYTHON FUNCTION, and the function is the closure
|
|
51
|
+
* `build_identity_tool`. The same leak is already in the shipped manifest
|
|
52
|
+
* (`output_schema.title` is `build_identity_toolDictOutput`), so the name is not an accident
|
|
53
|
+
* this port gets to tidy: it is on the wire today, and a client that pinned it would break.
|
|
54
|
+
*
|
|
55
|
+
* MEASURED SURVIVOR, AND IT STAYS. A mutation changing this one string to
|
|
56
|
+
* `build_identityArguments` survives both gates, because `build_identity` has no parameters
|
|
57
|
+
* and a model with no fields can never fail — the name is unreachable from the wire. It is
|
|
58
|
+
* still spelled correctly: the day the tool grows an argument, the sweep's kill comes for
|
|
59
|
+
* free, and a name that was wrong until then would be a defect nobody planted. The leak that
|
|
60
|
+
* IS observable is the manifest's `output_schema.title`, and that one is compared byte for
|
|
61
|
+
* byte by the conformance suite's `advertisement: id 2` case.
|
|
62
|
+
*/
|
|
63
|
+
export const ARG_MODELS = {
|
|
64
|
+
memory_save: {
|
|
65
|
+
model: 'memory_saveArguments',
|
|
66
|
+
fields: [req('type', 'str'), req('name', 'str'), req('description', 'str'), req('body', 'str'), opt('links', 'listStr')],
|
|
67
|
+
},
|
|
68
|
+
memory_recall: { model: 'memory_recallArguments', fields: [req('query', 'str'), opt('k', 'int')] },
|
|
69
|
+
validate_json: { model: 'validate_jsonArguments', fields: [req('output', 'str'), req('schema', 'dict')] },
|
|
70
|
+
shiftwork_clock_in: { model: 'shiftwork_clock_inArguments', fields: [req('checkpoint', 'str')] },
|
|
71
|
+
shiftwork_clock_out: {
|
|
72
|
+
model: 'shiftwork_clock_outArguments',
|
|
73
|
+
fields: [
|
|
74
|
+
req('checkpoint', 'str'),
|
|
75
|
+
req('unit_id', 'str'),
|
|
76
|
+
req('status', 'str'),
|
|
77
|
+
req('handoff_patch', 'dict'),
|
|
78
|
+
req('history_entry', 'dict'),
|
|
79
|
+
opt('accounting', 'dict'),
|
|
80
|
+
],
|
|
81
|
+
},
|
|
82
|
+
shiftwork_status: { model: 'shiftwork_statusArguments', fields: [req('checkpoint', 'str')] },
|
|
83
|
+
build_identity: { model: 'build_identity_toolArguments', fields: [] },
|
|
84
|
+
// `bantamkit_statusArguments`, not `bantamkit_status_toolArguments`: the model name is
|
|
85
|
+
// pydantic's, derived from the reference's CLOSURE name, and the status handler is called
|
|
86
|
+
// `bantamkit_status` there where the identity one is called `build_identity_tool`. It is
|
|
87
|
+
// the title `assets/tools/bantamkit_status.json` already advertises.
|
|
88
|
+
bantamkit_status: { model: 'bantamkit_statusArguments', fields: [] },
|
|
89
|
+
// `reserve: int | None = None` on the reference, so it is the same lax `int` as `k`:
|
|
90
|
+
// `'2'`, `True` and `3.0` validate, `'2.5'` is `int_parsing`, `2.5` is `int_from_float`,
|
|
91
|
+
// and an explicit `null` is the default. The manifest's `minimum: 0` is advisory to the
|
|
92
|
+
// client; nothing here floors it — a negative value reaches `MemoryStore.compact`, whose
|
|
93
|
+
// `max(0, min(reserve, budget // 2))` is the only clamp on either side (6b966e5).
|
|
94
|
+
memory_compact: { model: 'memory_compactArguments', fields: [opt('reserve', 'int')] },
|
|
95
|
+
// `bantamkit_read(path: str, part: str | None = None, offset: int | None = None,
|
|
96
|
+
// limit: int | None = None)` on the reference (job43). `path` and `part` are the strict
|
|
97
|
+
// `str` every other string field is — `123` is `string_type`, measured for `memory_save`
|
|
98
|
+
// and the same validator. `offset` and `limit` are the lax `int` that `k` and `reserve`
|
|
99
|
+
// are: `'2'`, `True` and `3.0` validate, `'2.5'` is `int_parsing`, `2.5` is
|
|
100
|
+
// `int_from_float`, and an explicit `null` is the default. The manifest's `minimum` and
|
|
101
|
+
// `maximum` are advisory to the client for `limit` and for `offset`'s floor; the handler
|
|
102
|
+
// clamps `limit` to `[1, 200]` and `offset` to `>= 0` itself, the way `memory_recall`
|
|
103
|
+
// clamps `k`. `offset`'s CEILING is bound in the model, as it is on the reference (F2).
|
|
104
|
+
bantamkit_read: {
|
|
105
|
+
model: 'bantamkit_readArguments',
|
|
106
|
+
fields: [req('path', 'str'), opt('part', 'str'), opt('offset', 'int', { le: OFFSET_MAXIMUM }), opt('limit', 'int')],
|
|
107
|
+
},
|
|
108
|
+
// `skill_audit(root: str, enabled: list[str] | None = None, usage: dict[str, int] | None =
|
|
109
|
+
// None, check: str | None = None, budget: int | None = None)` on the reference (job44).
|
|
110
|
+
// `usage` is the only `dict[str, int]` on the surface and the reason `dictInt` exists:
|
|
111
|
+
// pydantic validates a typed dict's VALUES, so `{'a': 'x'}` is `usage.a` / `int_parsing`
|
|
112
|
+
// where `handoff_patch`'s `dict[str, Any]` takes anything. `budget` carries no `le`,
|
|
113
|
+
// because the reference binds none — the manifest's `maximum` is advisory to the client,
|
|
114
|
+
// and the handler's own refusal is the negative one. `versions` is the only
|
|
115
|
+
// `dict[str, str]`, and the reason `dictStr` exists beside it: a `str` field is STRICT even
|
|
116
|
+
// in lax mode, so `{'a': 1}` is `versions.a` / `string_type` where the same value under
|
|
117
|
+
// `usage` would validate.
|
|
118
|
+
skill_audit: {
|
|
119
|
+
model: 'skill_auditArguments',
|
|
120
|
+
fields: [
|
|
121
|
+
req('root', 'str'),
|
|
122
|
+
opt('enabled', 'listStr'),
|
|
123
|
+
opt('usage', 'dictInt'),
|
|
124
|
+
opt('check', 'str'),
|
|
125
|
+
opt('budget', 'int'),
|
|
126
|
+
opt('versions', 'dictStr'),
|
|
127
|
+
],
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
/** `type(value).__name__`, for the `input_type=` half of the sentence. */
|
|
131
|
+
function pyTypeName(value) {
|
|
132
|
+
switch (value.t) {
|
|
133
|
+
case 'null':
|
|
134
|
+
return 'NoneType';
|
|
135
|
+
case 'bool':
|
|
136
|
+
return 'bool';
|
|
137
|
+
case 'int':
|
|
138
|
+
return 'int';
|
|
139
|
+
case 'float':
|
|
140
|
+
return 'float';
|
|
141
|
+
case 'str':
|
|
142
|
+
return 'str';
|
|
143
|
+
case 'list':
|
|
144
|
+
return 'list';
|
|
145
|
+
case 'dict':
|
|
146
|
+
return 'dict';
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
const encoder = new TextEncoder();
|
|
150
|
+
const decoder = new TextDecoder('utf-8');
|
|
151
|
+
/**
|
|
152
|
+
* pydantic-core's `truncate_input_value`: 50 UTF-8 bytes, then `head[..25] + "..." + tail[24..]`
|
|
153
|
+
* with both cuts moved off a multi-byte character rather than splitting one.
|
|
154
|
+
*
|
|
155
|
+
* The floor at each end is what makes 30 Thai characters render as 7 + `...` + 7 instead of
|
|
156
|
+
* 8 + `...` + 8 — measured, probe8 case 104.
|
|
157
|
+
*/
|
|
158
|
+
function truncateRepr(text) {
|
|
159
|
+
const bytes = encoder.encode(text);
|
|
160
|
+
if (bytes.length <= 50)
|
|
161
|
+
return text;
|
|
162
|
+
let head = 25;
|
|
163
|
+
while (head > 0 && (bytes[head] & 0xc0) === 0x80)
|
|
164
|
+
head -= 1;
|
|
165
|
+
let tail = bytes.length - 24;
|
|
166
|
+
while (tail < bytes.length && (bytes[tail] & 0xc0) === 0x80)
|
|
167
|
+
tail += 1;
|
|
168
|
+
return `${decoder.decode(bytes.slice(0, head))}...${decoder.decode(bytes.slice(tail))}`;
|
|
169
|
+
}
|
|
170
|
+
/** i64, which is the width pydantic parses an int into before it complains about size. */
|
|
171
|
+
const I64_MAX = 9223372036854775807n;
|
|
172
|
+
const I64_MIN = -9223372036854775808n;
|
|
173
|
+
/** `int(text)` as Python accepts it: optional sign, underscores between digits. */
|
|
174
|
+
const PY_INT_TEXT = /^[+-]?\d+(?:_\d+)*$/;
|
|
175
|
+
/** …and pydantic additionally accepts a trailing `.0`-style fraction that is all zeros. */
|
|
176
|
+
const PY_INT_DECIMAL_TEXT = /^([+-]?\d+(?:_\d+)*)\.0+$/;
|
|
177
|
+
/**
|
|
178
|
+
* Validate one field, returning either the coerced value or the error pydantic reports.
|
|
179
|
+
*
|
|
180
|
+
* Lax mode, which is pydantic's default and therefore the reference's: `bool` IS an `int`
|
|
181
|
+
* here, and a string that spells an integer IS one. Only `str` is strict, because pydantic
|
|
182
|
+
* refuses every non-`str` for a `str` field even in lax mode (measured: `123` -> string_type).
|
|
183
|
+
*/
|
|
184
|
+
function checkField(spec, value) {
|
|
185
|
+
if (spec.optional && value.t === 'null')
|
|
186
|
+
return { value };
|
|
187
|
+
switch (spec.kind) {
|
|
188
|
+
case 'str':
|
|
189
|
+
if (value.t === 'str')
|
|
190
|
+
return { value };
|
|
191
|
+
return [{ loc: spec.name, type: 'string_type', msg: 'Input should be a valid string', input: value }];
|
|
192
|
+
case 'dict':
|
|
193
|
+
if (value.t === 'dict')
|
|
194
|
+
return { value };
|
|
195
|
+
return [{ loc: spec.name, type: 'dict_type', msg: 'Input should be a valid dictionary', input: value }];
|
|
196
|
+
case 'dictStr': {
|
|
197
|
+
// `dict[str, str]`. The same shape as `dictInt` next door, with the STRICT `str`
|
|
198
|
+
// validator on the values: one error per bad entry, in the dict's own order, with the
|
|
199
|
+
// KEY in the location — `versions.a`, never `versions.0`.
|
|
200
|
+
if (value.t !== 'dict') {
|
|
201
|
+
return [{ loc: spec.name, type: 'dict_type', msg: 'Input should be a valid dictionary', input: value }];
|
|
202
|
+
}
|
|
203
|
+
const bad = [];
|
|
204
|
+
for (const [key, item] of value.v) {
|
|
205
|
+
if (item.t !== 'str') {
|
|
206
|
+
bad.push({
|
|
207
|
+
loc: `${spec.name}.${key}`,
|
|
208
|
+
type: 'string_type',
|
|
209
|
+
msg: 'Input should be a valid string',
|
|
210
|
+
input: item,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return bad.length > 0 ? bad : { value };
|
|
215
|
+
}
|
|
216
|
+
case 'dictInt': {
|
|
217
|
+
// `dict[str, int]`. pydantic validates the VALUES too, one error per bad entry and in
|
|
218
|
+
// the dict's own order, with the KEY in the location — `usage.a`, never `usage.0`. The
|
|
219
|
+
// coerced value is what the handler gets, so `{'a': '0'}` reaches `skillaudit` as `0`
|
|
220
|
+
// and is a `never-invoked` finding exactly as `{'a': 0}` is.
|
|
221
|
+
if (value.t !== 'dict') {
|
|
222
|
+
return [{ loc: spec.name, type: 'dict_type', msg: 'Input should be a valid dictionary', input: value }];
|
|
223
|
+
}
|
|
224
|
+
const bad = [];
|
|
225
|
+
const coerced = new Map();
|
|
226
|
+
for (const [key, item] of value.v) {
|
|
227
|
+
const checked = checkInt(`${spec.name}.${key}`, item);
|
|
228
|
+
if ('value' in checked)
|
|
229
|
+
coerced.set(key, checked.value);
|
|
230
|
+
else
|
|
231
|
+
bad.push(checked);
|
|
232
|
+
}
|
|
233
|
+
return bad.length > 0 ? bad : { value: { t: 'dict', v: coerced } };
|
|
234
|
+
}
|
|
235
|
+
case 'listStr': {
|
|
236
|
+
if (value.t !== 'list') {
|
|
237
|
+
return [{ loc: spec.name, type: 'list_type', msg: 'Input should be a valid list', input: value }];
|
|
238
|
+
}
|
|
239
|
+
// EVERY bad item, not the first: pydantic validates the whole list and reports one
|
|
240
|
+
// error per element, so `links: [1, 'ok', None]` is TWO errors, `links.0` and `links.2`.
|
|
241
|
+
const bad = [];
|
|
242
|
+
for (let i = 0; i < value.v.length; i += 1) {
|
|
243
|
+
const item = value.v[i];
|
|
244
|
+
if (item.t !== 'str') {
|
|
245
|
+
bad.push({ loc: `${spec.name}.${i}`, type: 'string_type', msg: 'Input should be a valid string', input: item });
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return bad.length > 0 ? bad : { value };
|
|
249
|
+
}
|
|
250
|
+
case 'int': {
|
|
251
|
+
const checked = checkInt(spec.name, value);
|
|
252
|
+
if (!('value' in checked))
|
|
253
|
+
return [checked];
|
|
254
|
+
// `less_than_equal` names the ORIGINAL input, as every constraint error does.
|
|
255
|
+
if (spec.le !== undefined && checked.value.t === 'int' && checked.value.v > spec.le) {
|
|
256
|
+
return [
|
|
257
|
+
{
|
|
258
|
+
loc: spec.name,
|
|
259
|
+
type: 'less_than_equal',
|
|
260
|
+
msg: `Input should be less than or equal to ${spec.le}`,
|
|
261
|
+
input: value,
|
|
262
|
+
},
|
|
263
|
+
];
|
|
264
|
+
}
|
|
265
|
+
return checked;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
function checkInt(loc, value) {
|
|
270
|
+
const tooBig = () => ({
|
|
271
|
+
loc,
|
|
272
|
+
type: 'int_parsing_size',
|
|
273
|
+
msg: 'Unable to parse input string as an integer, exceeded maximum size',
|
|
274
|
+
input: value,
|
|
275
|
+
});
|
|
276
|
+
if (value.t === 'int') {
|
|
277
|
+
return value.v > I64_MAX || value.v < I64_MIN ? tooBig() : { value };
|
|
278
|
+
}
|
|
279
|
+
if (value.t === 'bool')
|
|
280
|
+
return { value: { t: 'int', v: value.v ? 1n : 0n } };
|
|
281
|
+
if (value.t === 'float') {
|
|
282
|
+
if (!Number.isInteger(value.v)) {
|
|
283
|
+
return {
|
|
284
|
+
loc,
|
|
285
|
+
type: 'int_from_float',
|
|
286
|
+
msg: 'Input should be a valid integer, got a number with a fractional part',
|
|
287
|
+
input: value,
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
if (!(Math.abs(value.v) <= 9223372036854775807))
|
|
291
|
+
return tooBig();
|
|
292
|
+
return { value: { t: 'int', v: BigInt(value.v) } };
|
|
293
|
+
}
|
|
294
|
+
if (value.t === 'str') {
|
|
295
|
+
const text = value.v.trim();
|
|
296
|
+
const decimal = PY_INT_DECIMAL_TEXT.exec(text);
|
|
297
|
+
const digits = PY_INT_TEXT.test(text) ? text : decimal?.[1];
|
|
298
|
+
if (digits === undefined) {
|
|
299
|
+
return {
|
|
300
|
+
loc,
|
|
301
|
+
type: 'int_parsing',
|
|
302
|
+
msg: 'Input should be a valid integer, unable to parse string as an integer',
|
|
303
|
+
input: value,
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
const parsed = BigInt(digits.replace(/_/g, ''));
|
|
307
|
+
return parsed > I64_MAX || parsed < I64_MIN ? tooBig() : { value: { t: 'int', v: parsed } };
|
|
308
|
+
}
|
|
309
|
+
return { loc, type: 'int_type', msg: 'Input should be a valid integer', input: value };
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* The pydantic `ValidationError` text, WITHOUT the `Error executing tool <name>: ` prefix.
|
|
313
|
+
*
|
|
314
|
+
* The prefix is the SDK's, added by `Tool.run`'s `except Exception` for every failure a tool
|
|
315
|
+
* can have — not just an argument one — so it belongs at the one place that catches them all.
|
|
316
|
+
*/
|
|
317
|
+
export class PyValidationFailure extends Error {
|
|
318
|
+
detail;
|
|
319
|
+
constructor(detail) {
|
|
320
|
+
super(detail);
|
|
321
|
+
this.detail = detail;
|
|
322
|
+
this.name = 'PyValidationFailure';
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* `model.model_validate(arguments)` for one tool, or the exact `ValidationError` text.
|
|
327
|
+
*
|
|
328
|
+
* Unknown keys are IGNORED, because pydantic's default `model_config` ignores them and the
|
|
329
|
+
* measured server accepts `{"query": "a", "extra": 1}` without complaint. The absent-key case
|
|
330
|
+
* and the explicit-`null` case are different: `{}` is `missing`, `{"handoff_patch": null}` is
|
|
331
|
+
* `dict_type`, and only an OPTIONAL field treats `null` as its default.
|
|
332
|
+
*/
|
|
333
|
+
export function validateArguments(model, args) {
|
|
334
|
+
const supplied = args.t === 'dict' ? args.v : new Map();
|
|
335
|
+
const errors = [];
|
|
336
|
+
const bound = new Map();
|
|
337
|
+
for (const spec of model.fields) {
|
|
338
|
+
if (!supplied.has(spec.name)) {
|
|
339
|
+
if (spec.optional) {
|
|
340
|
+
bound.set(spec.name, { t: 'null' });
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
errors.push({ loc: spec.name, type: 'missing', msg: 'Field required', input: args });
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
const checked = checkField(spec, supplied.get(spec.name));
|
|
347
|
+
if (Array.isArray(checked))
|
|
348
|
+
errors.push(...checked);
|
|
349
|
+
else
|
|
350
|
+
bound.set(spec.name, checked.value);
|
|
351
|
+
}
|
|
352
|
+
if (errors.length === 0)
|
|
353
|
+
return bound;
|
|
354
|
+
const count = errors.length;
|
|
355
|
+
const head = `${count} validation error${count === 1 ? '' : 's'} for ${model.model}`;
|
|
356
|
+
const body = errors
|
|
357
|
+
.map((e) => `${e.loc}\n ${e.msg} [type=${e.type}, input_value=${truncateRepr(reprValue(e.input))}, ` +
|
|
358
|
+
`input_type=${pyTypeName(e.input)}]\n For further information visit ` +
|
|
359
|
+
`https://errors.pydantic.dev/2.13/v/${e.type}`)
|
|
360
|
+
.join('\n');
|
|
361
|
+
throw new PyValidationFailure(`${head}\n${body}`);
|
|
362
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The SDK's serializer — which is NOT `json.dumps`, and that is the whole reason this file
|
|
3
|
+
* exists next to `pyjson.ts`.
|
|
4
|
+
*
|
|
5
|
+
* Two different writers reach the wire from the Python server and only one of them is
|
|
6
|
+
* CPython's:
|
|
7
|
+
*
|
|
8
|
+
* * every FILE bantamkit writes (`checkpoint.json`, `.log.jsonl`) goes through
|
|
9
|
+
* `json.dumps` — `ensure_ascii=True`, `", "`/`": "` separators, codepoint `sort_keys`.
|
|
10
|
+
* That is `pyjson.dumpJson`, N6's, and nothing here replaces it.
|
|
11
|
+
* * every JSON-RPC FRAME, and the `content[0].text` inside a tool result, goes through
|
|
12
|
+
* `pydantic_core.to_json` — Rust `serde_json`, which escapes nothing outside the JSON
|
|
13
|
+
* minimum, emits raw UTF-8, and formats floats with ryu.
|
|
14
|
+
*
|
|
15
|
+
* Measured, `mcp` 2.0.0 / pydantic-core 2.x (`scratchpad/job38-n7/tj.py`):
|
|
16
|
+
*
|
|
17
|
+
* to_json({'x': 'em — dash'}) -> {"x":"em — dash"} (NOT —)
|
|
18
|
+
* to_json({'k': '\x7f\x01'}) -> {"k":"?"} -> DEL RAW, escaped
|
|
19
|
+
* to_json(1e-6) -> 1e-6 while repr(1e-6) is '1e-06'
|
|
20
|
+
* to_json(1e-5) -> 0.00001 while repr(1e-5) is '1e-05'
|
|
21
|
+
* to_json(1.0) -> 1.0 while String(1.0) is '1'
|
|
22
|
+
*
|
|
23
|
+
* The first two are what `JSON.stringify` already does, so strings ride on it. The last two
|
|
24
|
+
* are not, and are why numbers are formatted by hand: a `5.0` that arrived on the wire and
|
|
25
|
+
* left as `5` is exactly the defect N6 measured on the file side, relocated to the frame.
|
|
26
|
+
*/
|
|
27
|
+
import type { PyValue } from '../pyjson.js';
|
|
28
|
+
/**
|
|
29
|
+
* ryu's presentation rule, as `serde_json` uses it, derived by sweeping 99 values against
|
|
30
|
+
* the running `pydantic_core` (`scratchpad/job38-n7/tf2.py`).
|
|
31
|
+
*
|
|
32
|
+
* It agrees with CPython's `repr` everywhere except the exponent, in two ways:
|
|
33
|
+
* 1. the exponent is NOT zero-padded — `1e-6`, not `1e-06`;
|
|
34
|
+
* 2. the decimal/scientific boundary on the small side is one decade lower — `1e-5`
|
|
35
|
+
* prints as `0.00001` where `repr` gives `1e-05`.
|
|
36
|
+
* The large side agrees: both switch to scientific at 1e16.
|
|
37
|
+
*
|
|
38
|
+
* `-0.0` keeps its sign in both. NaN and the infinities cannot occur: they are not JSON,
|
|
39
|
+
* every value here arrived by parsing a JSON document, and `pyjson.parseJson` rejects
|
|
40
|
+
* Python's `NaN`/`Infinity` extension the way a strict reader must.
|
|
41
|
+
*/
|
|
42
|
+
export declare function serdeFloat(x: number): string;
|
|
43
|
+
/**
|
|
44
|
+
* `pydantic_core.to_json(value, indent=<indent>)`.
|
|
45
|
+
*
|
|
46
|
+
* `indent = null` is the compact form the JSON-RPC frame uses — `,` and `:` with NO spaces,
|
|
47
|
+
* which is serde's default and NOT `json.dumps`' default. `indent = 2` is the form
|
|
48
|
+
* `_convert_to_content` uses for the text block of every dict-returning tool.
|
|
49
|
+
*
|
|
50
|
+
* Strings go through `JSON.stringify`, which was checked against serde over the escape
|
|
51
|
+
* boundary: both emit the short escapes for `\b \t \n \f \r " \\`, `\u00XX` for the other
|
|
52
|
+
* C0 controls, and raw bytes for everything else including DEL and `/`. They part company
|
|
53
|
+
* only on a LONE SURROGATE, which JS escapes and pydantic-core refuses to encode at all —
|
|
54
|
+
* unreachable here, because a lone surrogate cannot survive UTF-8 decoding of the wire.
|
|
55
|
+
*/
|
|
56
|
+
export declare function sdkJson(value: PyValue, indent?: number | null): string;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ryu's presentation rule, as `serde_json` uses it, derived by sweeping 99 values against
|
|
3
|
+
* the running `pydantic_core` (`scratchpad/job38-n7/tf2.py`).
|
|
4
|
+
*
|
|
5
|
+
* It agrees with CPython's `repr` everywhere except the exponent, in two ways:
|
|
6
|
+
* 1. the exponent is NOT zero-padded — `1e-6`, not `1e-06`;
|
|
7
|
+
* 2. the decimal/scientific boundary on the small side is one decade lower — `1e-5`
|
|
8
|
+
* prints as `0.00001` where `repr` gives `1e-05`.
|
|
9
|
+
* The large side agrees: both switch to scientific at 1e16.
|
|
10
|
+
*
|
|
11
|
+
* `-0.0` keeps its sign in both. NaN and the infinities cannot occur: they are not JSON,
|
|
12
|
+
* every value here arrived by parsing a JSON document, and `pyjson.parseJson` rejects
|
|
13
|
+
* Python's `NaN`/`Infinity` extension the way a strict reader must.
|
|
14
|
+
*/
|
|
15
|
+
export function serdeFloat(x) {
|
|
16
|
+
if (!Number.isFinite(x)) {
|
|
17
|
+
// `to_json` writes `null` for a non-finite float; unreachable from parsed JSON, kept so
|
|
18
|
+
// the function is total rather than silently emitting invalid JSON.
|
|
19
|
+
return 'null';
|
|
20
|
+
}
|
|
21
|
+
const sign = Object.is(x, -0) || x < 0 ? '-' : '';
|
|
22
|
+
const abs = Math.abs(x);
|
|
23
|
+
if (abs === 0)
|
|
24
|
+
return `${sign}0.0`;
|
|
25
|
+
// `toExponential()` with no argument gives the SHORTEST round-tripping digits, which is
|
|
26
|
+
// the same digit string ryu produces; only the presentation is ours.
|
|
27
|
+
const [mantissa, exponentText] = abs.toExponential().split('e');
|
|
28
|
+
const exponent = Number(exponentText);
|
|
29
|
+
const digits = mantissa.replace('.', '');
|
|
30
|
+
if (exponent >= -5 && exponent < 16) {
|
|
31
|
+
if (exponent >= 0) {
|
|
32
|
+
const whole = digits.padEnd(exponent + 1, '0').slice(0, exponent + 1);
|
|
33
|
+
const fraction = digits.slice(exponent + 1);
|
|
34
|
+
return `${sign}${whole}.${fraction === '' ? '0' : fraction}`;
|
|
35
|
+
}
|
|
36
|
+
return `${sign}0.${'0'.repeat(-exponent - 1)}${digits}`;
|
|
37
|
+
}
|
|
38
|
+
const head = digits.length === 1 ? digits : `${digits[0]}.${digits.slice(1)}`;
|
|
39
|
+
return `${sign}${head}e${exponent < 0 ? '-' : '+'}${Math.abs(exponent)}`;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* `pydantic_core.to_json(value, indent=<indent>)`.
|
|
43
|
+
*
|
|
44
|
+
* `indent = null` is the compact form the JSON-RPC frame uses — `,` and `:` with NO spaces,
|
|
45
|
+
* which is serde's default and NOT `json.dumps`' default. `indent = 2` is the form
|
|
46
|
+
* `_convert_to_content` uses for the text block of every dict-returning tool.
|
|
47
|
+
*
|
|
48
|
+
* Strings go through `JSON.stringify`, which was checked against serde over the escape
|
|
49
|
+
* boundary: both emit the short escapes for `\b \t \n \f \r " \\`, `\u00XX` for the other
|
|
50
|
+
* C0 controls, and raw bytes for everything else including DEL and `/`. They part company
|
|
51
|
+
* only on a LONE SURROGATE, which JS escapes and pydantic-core refuses to encode at all —
|
|
52
|
+
* unreachable here, because a lone surrogate cannot survive UTF-8 decoding of the wire.
|
|
53
|
+
*/
|
|
54
|
+
export function sdkJson(value, indent = null) {
|
|
55
|
+
const pad = indent === null ? '' : ' '.repeat(indent);
|
|
56
|
+
const itemSep = indent === null ? ',' : ',';
|
|
57
|
+
const keySep = indent === null ? ':' : ': ';
|
|
58
|
+
const encode = (v, level) => {
|
|
59
|
+
switch (v.t) {
|
|
60
|
+
case 'null':
|
|
61
|
+
return 'null';
|
|
62
|
+
case 'bool':
|
|
63
|
+
return v.v ? 'true' : 'false';
|
|
64
|
+
case 'int':
|
|
65
|
+
return v.v.toString();
|
|
66
|
+
case 'float':
|
|
67
|
+
return serdeFloat(v.v);
|
|
68
|
+
case 'str':
|
|
69
|
+
return JSON.stringify(v.v);
|
|
70
|
+
case 'list': {
|
|
71
|
+
if (v.v.length === 0)
|
|
72
|
+
return '[]';
|
|
73
|
+
const parts = v.v.map((item) => encode(item, level + 1));
|
|
74
|
+
if (indent === null)
|
|
75
|
+
return `[${parts.join(itemSep)}]`;
|
|
76
|
+
const nl = `\n${pad.repeat(level + 1)}`;
|
|
77
|
+
return `[${nl}${parts.join(itemSep + nl)}\n${pad.repeat(level)}]`;
|
|
78
|
+
}
|
|
79
|
+
case 'dict': {
|
|
80
|
+
if (v.v.size === 0)
|
|
81
|
+
return '{}';
|
|
82
|
+
const parts = [...v.v].map(([k, item]) => `${JSON.stringify(k)}${keySep}${encode(item, level + 1)}`);
|
|
83
|
+
if (indent === null)
|
|
84
|
+
return `{${parts.join(itemSep)}}`;
|
|
85
|
+
const nl = `\n${pad.repeat(level + 1)}`;
|
|
86
|
+
return `{${nl}${parts.join(itemSep + nl)}\n${pad.repeat(level)}}`;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
return encode(value, 0);
|
|
91
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
2
|
+
import { EventLog } from '../eventlog.js';
|
|
3
|
+
import type { Memory } from '../memory/component.js';
|
|
4
|
+
import type { RawStdioTransport } from './transport.js';
|
|
5
|
+
/**
|
|
6
|
+
* The eleven, in the order `build_server` lists them — which is the order `tools/list` emits.
|
|
7
|
+
*
|
|
8
|
+
* `bantamkit_status` went LAST rather than first, `memory_compact` after it rather than
|
|
9
|
+
* beside `memory_save` where a reader would look for it, `bantamkit_read` after that and
|
|
10
|
+
* `skill_audit` after that, for the same reason the reference appends all four: registration
|
|
11
|
+
* order IS the served order, and appending is the only edit that leaves the other ten where
|
|
12
|
+
* every existing declaration says they are.
|
|
13
|
+
*/
|
|
14
|
+
export declare const MCP_TOOLS: readonly ["memory_save", "memory_recall", "validate_json", "shiftwork_clock_in", "shiftwork_clock_out", "shiftwork_status", "build_identity", "bantamkit_status", "memory_compact", "bantamkit_read", "skill_audit"];
|
|
15
|
+
/**
|
|
16
|
+
* The advertisement for one tool, out of its manifest, with the surface checked.
|
|
17
|
+
*
|
|
18
|
+
* Key order is `description, inputSchema, name, outputSchema` because that is the order the
|
|
19
|
+
* reference's pydantic `Tool` model dumps in, and the nested schemas keep the manifest's own
|
|
20
|
+
* key order because both runtimes preserve insertion order in a JSON object.
|
|
21
|
+
*
|
|
22
|
+
* `output_schema` is advertised VERBATIM and is not derived from anything this file knows.
|
|
23
|
+
* That includes `build_identity.json`'s `"title": "build_identity_toolDictOutput"`, which
|
|
24
|
+
* leaks the Python closure name `build_identity_tool` into a Node server's advertisement.
|
|
25
|
+
* IT IS REPRODUCED, NOT FIXED. The manifest is an asset, `build_identity` hashes every asset
|
|
26
|
+
* byte, and changing it would move `assets_digest` — the one field that is supposed to be
|
|
27
|
+
* comparable across the two runtimes. A cosmetic title is not worth breaking the instrument
|
|
28
|
+
* that detects drift. Registered here, not fixed here; `runtime-py` is the reference.
|
|
29
|
+
*/
|
|
30
|
+
export declare function fromManifest(name: string): Record<string, unknown>;
|
|
31
|
+
/**
|
|
32
|
+
* Assemble the MCP server around one Memory instance (the per-person state).
|
|
33
|
+
*
|
|
34
|
+
* `wire` is the transport, and it is a parameter rather than something this function creates
|
|
35
|
+
* because the handlers need it twice: to read the RAW request line (so `5.0` is still a float
|
|
36
|
+
* when it reaches `shiftwork.clockOut`) and to register the EXACT result text (so it is still
|
|
37
|
+
* a float on the way back out).
|
|
38
|
+
*
|
|
39
|
+
* `log` is the event-log sink (`docs/eventlog.md`), resolved from `BANTAMKIT_EVENT_LOG` when
|
|
40
|
+
* the caller does not supply one and DISABLED unless that variable asks for it. It is a
|
|
41
|
+
* parameter and not only an environment read so that a test can inject a fixed clock and a
|
|
42
|
+
* scratch path without setting a process-wide variable — the same seam `runtime-py`'s
|
|
43
|
+
* `build_server(memory, log=None)` offers.
|
|
44
|
+
*/
|
|
45
|
+
export declare function buildServer(memory: Memory, wire: RawStdioTransport, version: string, log?: EventLog): Server;
|