aontu 0.52.1 → 0.53.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 (272) hide show
  1. package/README.md +88 -0
  2. package/bin/aontu-mcp.js +4 -0
  3. package/dist/agentsmd.d.ts +16 -0
  4. package/dist/agentsmd.js +107 -0
  5. package/dist/agentsmd.js.map +1 -0
  6. package/dist/aontu.d.ts +14 -3
  7. package/dist/aontu.js +96 -4
  8. package/dist/aontu.js.map +1 -1
  9. package/dist/cli.d.ts +44 -1
  10. package/dist/cli.js +2401 -44
  11. package/dist/cli.js.map +1 -1
  12. package/dist/ctx.d.ts +13 -0
  13. package/dist/ctx.js +43 -0
  14. package/dist/ctx.js.map +1 -1
  15. package/dist/diff.d.ts +22 -0
  16. package/dist/diff.js +141 -0
  17. package/dist/diff.js.map +1 -0
  18. package/dist/err.d.ts +3 -1
  19. package/dist/err.js +38 -7
  20. package/dist/err.js.map +1 -1
  21. package/dist/graph.d.ts +16 -0
  22. package/dist/graph.js +73 -0
  23. package/dist/graph.js.map +1 -0
  24. package/dist/hcanon.d.ts +3 -0
  25. package/dist/hcanon.js +146 -0
  26. package/dist/hcanon.js.map +1 -0
  27. package/dist/hints.js +167 -4
  28. package/dist/hints.js.map +1 -1
  29. package/dist/jsonschema.d.ts +20 -0
  30. package/dist/jsonschema.js +391 -0
  31. package/dist/jsonschema.js.map +1 -0
  32. package/dist/lang.js +512 -69
  33. package/dist/lang.js.map +1 -1
  34. package/dist/lsp.d.ts +9 -2
  35. package/dist/lsp.js +262 -47
  36. package/dist/lsp.js.map +1 -1
  37. package/dist/mcp-server.d.ts +20 -0
  38. package/dist/mcp-server.js +147 -0
  39. package/dist/mcp-server.js.map +1 -0
  40. package/dist/mcp.d.ts +42 -0
  41. package/dist/mcp.js +814 -0
  42. package/dist/mcp.js.map +1 -0
  43. package/dist/mod-tool.d.ts +58 -0
  44. package/dist/mod-tool.js +498 -0
  45. package/dist/mod-tool.js.map +1 -0
  46. package/dist/mod.d.ts +31 -0
  47. package/dist/mod.js +250 -0
  48. package/dist/mod.js.map +1 -0
  49. package/dist/patch.d.ts +44 -0
  50. package/dist/patch.js +506 -0
  51. package/dist/patch.js.map +1 -0
  52. package/dist/provenance.d.ts +40 -0
  53. package/dist/provenance.js +335 -0
  54. package/dist/provenance.js.map +1 -0
  55. package/dist/query.d.ts +27 -0
  56. package/dist/query.js +294 -0
  57. package/dist/query.js.map +1 -0
  58. package/dist/reach.d.ts +14 -0
  59. package/dist/reach.js +140 -0
  60. package/dist/reach.js.map +1 -0
  61. package/dist/relation.d.ts +19 -0
  62. package/dist/relation.js +305 -0
  63. package/dist/relation.js.map +1 -0
  64. package/dist/report-sarif.d.ts +14 -0
  65. package/dist/report-sarif.js +102 -0
  66. package/dist/report-sarif.js.map +1 -0
  67. package/dist/site.d.ts +4 -0
  68. package/dist/site.js +31 -0
  69. package/dist/site.js.map +1 -1
  70. package/dist/std.d.ts +1 -0
  71. package/dist/std.js +73 -0
  72. package/dist/std.js.map +1 -0
  73. package/dist/subsume.d.ts +39 -0
  74. package/dist/subsume.js +526 -0
  75. package/dist/subsume.js.map +1 -0
  76. package/dist/trim.d.ts +19 -0
  77. package/dist/trim.js +155 -0
  78. package/dist/trim.js.map +1 -0
  79. package/dist/tsconfig.tsbuildinfo +1 -1
  80. package/dist/type.d.ts +17 -1
  81. package/dist/type.js.map +1 -1
  82. package/dist/unify.d.ts +2 -1
  83. package/dist/unify.js +253 -36
  84. package/dist/unify.js.map +1 -1
  85. package/dist/utility.d.ts +9 -1
  86. package/dist/utility.js +122 -1
  87. package/dist/utility.js.map +1 -1
  88. package/dist/val/AggFuncVal.d.ts +33 -0
  89. package/dist/val/AggFuncVal.js +202 -0
  90. package/dist/val/AggFuncVal.js.map +1 -0
  91. package/dist/val/ArithFuncVal.d.ts +31 -0
  92. package/dist/val/ArithFuncVal.js +62 -0
  93. package/dist/val/ArithFuncVal.js.map +1 -0
  94. package/dist/val/BagVal.d.ts +5 -0
  95. package/dist/val/BagVal.js +96 -5
  96. package/dist/val/BagVal.js.map +1 -1
  97. package/dist/val/CloseFuncVal.js +9 -1
  98. package/dist/val/CloseFuncVal.js.map +1 -1
  99. package/dist/val/ConjunctVal.d.ts +1 -1
  100. package/dist/val/ConjunctVal.js +19 -0
  101. package/dist/val/ConjunctVal.js.map +1 -1
  102. package/dist/val/ConstraintVal.d.ts +7 -1
  103. package/dist/val/ConstraintVal.js +490 -40
  104. package/dist/val/ConstraintVal.js.map +1 -1
  105. package/dist/val/CopyFuncVal.d.ts +1 -2
  106. package/dist/val/CopyFuncVal.js +7 -0
  107. package/dist/val/CopyFuncVal.js.map +1 -1
  108. package/dist/val/Decimal.d.ts +1 -0
  109. package/dist/val/Decimal.js +13 -0
  110. package/dist/val/Decimal.js.map +1 -1
  111. package/dist/val/DeprecateFuncVal.d.ts +11 -0
  112. package/dist/val/DeprecateFuncVal.js +47 -0
  113. package/dist/val/DeprecateFuncVal.js.map +1 -0
  114. package/dist/val/DisjunctVal.js +130 -21
  115. package/dist/val/DisjunctVal.js.map +1 -1
  116. package/dist/val/EachFuncVal.d.ts +15 -0
  117. package/dist/val/EachFuncVal.js +75 -0
  118. package/dist/val/EachFuncVal.js.map +1 -0
  119. package/dist/val/ExpectVal.js +27 -4
  120. package/dist/val/ExpectVal.js.map +1 -1
  121. package/dist/val/FeatureVal.js +1 -1
  122. package/dist/val/FeatureVal.js.map +1 -1
  123. package/dist/val/FilterFuncVal.d.ts +15 -0
  124. package/dist/val/FilterFuncVal.js +91 -0
  125. package/dist/val/FilterFuncVal.js.map +1 -0
  126. package/dist/val/FuncBaseVal.d.ts +6 -1
  127. package/dist/val/FuncBaseVal.js +171 -1
  128. package/dist/val/FuncBaseVal.js.map +1 -1
  129. package/dist/val/HideFuncVal.js.map +1 -1
  130. package/dist/val/IdFuncVal.d.ts +13 -0
  131. package/dist/val/IdFuncVal.js +54 -0
  132. package/dist/val/IdFuncVal.js.map +1 -0
  133. package/dist/val/JunctionVal.js +7 -1
  134. package/dist/val/JunctionVal.js.map +1 -1
  135. package/dist/val/KeyFuncVal.d.ts +1 -1
  136. package/dist/val/KeyFuncVal.js +38 -30
  137. package/dist/val/KeyFuncVal.js.map +1 -1
  138. package/dist/val/ListVal.js +101 -14
  139. package/dist/val/ListVal.js.map +1 -1
  140. package/dist/val/LowerFuncVal.js.map +1 -1
  141. package/dist/val/MapVal.js +93 -8
  142. package/dist/val/MapVal.js.map +1 -1
  143. package/dist/val/MatchFuncVal.d.ts +15 -0
  144. package/dist/val/MatchFuncVal.js +107 -0
  145. package/dist/val/MatchFuncVal.js.map +1 -0
  146. package/dist/val/MoveFuncVal.js.map +1 -1
  147. package/dist/val/NilVal.js +24 -0
  148. package/dist/val/NilVal.js.map +1 -1
  149. package/dist/val/OpBaseVal.d.ts +1 -1
  150. package/dist/val/OpBaseVal.js +19 -1
  151. package/dist/val/OpBaseVal.js.map +1 -1
  152. package/dist/val/OpenFuncVal.js +4 -1
  153. package/dist/val/OpenFuncVal.js.map +1 -1
  154. package/dist/val/PackFuncVal.d.ts +15 -0
  155. package/dist/val/PackFuncVal.js +108 -0
  156. package/dist/val/PackFuncVal.js.map +1 -0
  157. package/dist/val/PathFuncVal.js.map +1 -1
  158. package/dist/val/PlaceVal.d.ts +13 -0
  159. package/dist/val/PlaceVal.js +131 -0
  160. package/dist/val/PlaceVal.js.map +1 -0
  161. package/dist/val/PlusOpVal.js +11 -2
  162. package/dist/val/PlusOpVal.js.map +1 -1
  163. package/dist/val/PrefFuncVal.js.map +1 -1
  164. package/dist/val/PrefVal.d.ts +2 -2
  165. package/dist/val/PrefVal.js +78 -23
  166. package/dist/val/PrefVal.js.map +1 -1
  167. package/dist/val/RefVal.d.ts +1 -1
  168. package/dist/val/RefVal.js +89 -7
  169. package/dist/val/RefVal.js.map +1 -1
  170. package/dist/val/ReferFuncVal.d.ts +36 -0
  171. package/dist/val/ReferFuncVal.js +303 -0
  172. package/dist/val/ReferFuncVal.js.map +1 -0
  173. package/dist/val/ScalarKindVal.d.ts +1 -2
  174. package/dist/val/ScalarKindVal.js +0 -11
  175. package/dist/val/ScalarKindVal.js.map +1 -1
  176. package/dist/val/TopVal.js.map +1 -1
  177. package/dist/val/TypeFuncVal.js.map +1 -1
  178. package/dist/val/UpperFuncVal.js.map +1 -1
  179. package/dist/val/Val.d.ts +8 -1
  180. package/dist/val/Val.js +150 -1
  181. package/dist/val/Val.js.map +1 -1
  182. package/dist/val/VarVal.js.map +1 -1
  183. package/dist/val/arith.d.ts +6 -0
  184. package/dist/val/arith.js +170 -0
  185. package/dist/val/arith.js.map +1 -0
  186. package/dist/vet.d.ts +45 -0
  187. package/dist/vet.js +776 -0
  188. package/dist/vet.js.map +1 -0
  189. package/dist/walk.d.ts +2 -0
  190. package/dist/walk.js +91 -0
  191. package/dist/walk.js.map +1 -0
  192. package/grammar/aontu.gbnf +130 -0
  193. package/grammar/aontu.lark +113 -0
  194. package/package.json +21 -6
  195. package/skill/SKILL.md +37 -0
  196. package/skill/error-codes.md +62 -0
  197. package/skill/examples.md +99 -0
  198. package/skill/grammar-card.md +57 -0
  199. package/src/agentsmd.ts +135 -0
  200. package/src/aontu.ts +135 -4
  201. package/src/cli.ts +2858 -71
  202. package/src/ctx.ts +73 -1
  203. package/src/diff.ts +196 -0
  204. package/src/err.ts +42 -7
  205. package/src/graph.ts +135 -0
  206. package/src/hcanon.ts +169 -0
  207. package/src/hints.ts +208 -4
  208. package/src/jsonschema.ts +511 -0
  209. package/src/lang.ts +570 -70
  210. package/src/lsp.ts +281 -48
  211. package/src/mcp-server.ts +187 -0
  212. package/src/mcp.ts +993 -0
  213. package/src/mod-tool.ts +679 -0
  214. package/src/mod.ts +344 -0
  215. package/src/patch.ts +624 -0
  216. package/src/provenance.ts +430 -0
  217. package/src/query.ts +379 -0
  218. package/src/reach.ts +184 -0
  219. package/src/relation.ts +395 -0
  220. package/src/report-sarif.ts +137 -0
  221. package/src/site.ts +36 -1
  222. package/src/std.ts +73 -0
  223. package/src/subsume.ts +690 -0
  224. package/src/trim.ts +195 -0
  225. package/src/tsconfig.json +10 -4
  226. package/src/type.ts +51 -2
  227. package/src/unify.ts +274 -36
  228. package/src/utility.ts +139 -1
  229. package/src/val/AggFuncVal.ts +319 -0
  230. package/src/val/ArithFuncVal.ts +108 -0
  231. package/src/val/BagVal.ts +101 -4
  232. package/src/val/CloseFuncVal.ts +9 -1
  233. package/src/val/ConjunctVal.ts +20 -0
  234. package/src/val/ConstraintVal.ts +542 -43
  235. package/src/val/CopyFuncVal.ts +7 -1
  236. package/src/val/Decimal.ts +15 -0
  237. package/src/val/DeprecateFuncVal.ts +84 -0
  238. package/src/val/DisjunctVal.ts +139 -28
  239. package/src/val/EachFuncVal.ts +133 -0
  240. package/src/val/ExpectVal.ts +28 -6
  241. package/src/val/FeatureVal.ts +1 -1
  242. package/src/val/FilterFuncVal.ts +154 -0
  243. package/src/val/FuncBaseVal.ts +192 -1
  244. package/src/val/HideFuncVal.ts +0 -2
  245. package/src/val/IdFuncVal.ts +91 -0
  246. package/src/val/JunctionVal.ts +7 -1
  247. package/src/val/KeyFuncVal.ts +39 -35
  248. package/src/val/ListVal.ts +108 -15
  249. package/src/val/LowerFuncVal.ts +0 -1
  250. package/src/val/MapVal.ts +99 -8
  251. package/src/val/MatchFuncVal.ts +176 -0
  252. package/src/val/MoveFuncVal.ts +0 -2
  253. package/src/val/NilVal.ts +25 -0
  254. package/src/val/OpBaseVal.ts +20 -1
  255. package/src/val/OpenFuncVal.ts +4 -2
  256. package/src/val/PackFuncVal.ts +175 -0
  257. package/src/val/PathFuncVal.ts +0 -1
  258. package/src/val/PlaceVal.ts +193 -0
  259. package/src/val/PlusOpVal.ts +11 -2
  260. package/src/val/PrefFuncVal.ts +0 -1
  261. package/src/val/PrefVal.ts +79 -36
  262. package/src/val/RefVal.ts +91 -8
  263. package/src/val/ReferFuncVal.ts +387 -0
  264. package/src/val/ScalarKindVal.ts +0 -13
  265. package/src/val/TopVal.ts +0 -1
  266. package/src/val/TypeFuncVal.ts +0 -2
  267. package/src/val/UpperFuncVal.ts +0 -1
  268. package/src/val/Val.ts +205 -2
  269. package/src/val/VarVal.ts +0 -1
  270. package/src/val/arith.ts +316 -0
  271. package/src/vet.ts +992 -0
  272. package/src/walk.ts +99 -0
package/src/mcp.ts ADDED
@@ -0,0 +1,993 @@
1
+ /* Copyright (c) 2025 Richard Rodger, MIT License */
2
+
3
+ // THE MCP TOOL LIBRARY (G7 phase 6,
4
+ // docs/capability-review/g7-machine-access.md; completed to the full
5
+ // CLI verb surface by the use-case review's MCP recommendation,
6
+ // use-cases/SUPPORT.md): the verbs an agent calls, over the Model
7
+ // Context Protocol, as a transport-free library. The split follows
8
+ // the LSP's (docs/lsp.md): this file is the protocol and the tools,
9
+ // ts/src/mcp-server.ts is stdio and nothing else, and the whole thing
10
+ // is testable without a socket.
11
+ //
12
+ // Every tool returns THE SAME JSON CONTRACT THE CLI PRINTS. That is
13
+ // the point of the surface: an agent that has read `aontu vet
14
+ // --format json` output knows what the `vet` tool answers, and a
15
+ // report copied from one to the other is the same object. The tools
16
+ // add no vocabulary of their own.
17
+ //
18
+ // The server evaluates under a CONFINED resolver (G5, docs/trust.md):
19
+ // a caller hands source text, and text that could reach out through
20
+ // `@"..."` is exactly what a server must not run unconfined. By
21
+ // default every include is denied; a server started with
22
+ // `--root <dir>` (ts/src/mcp-server.ts) resolves includes confined
23
+ // below that root instead — the CLI's `--trust root:<dir>` posture —
24
+ // and lets every document argument arrive as a `<name>Path` file
25
+ // under the same root. The package-resolver leg is never enabled
26
+ // here.
27
+
28
+ import { readFileSync, realpathSync } from 'node:fs'
29
+ import {
30
+ basename as pathBasename,
31
+ dirname as pathDirname,
32
+ join as pathJoin,
33
+ resolve as pathResolve,
34
+ sep as pathSep,
35
+ } from 'node:path'
36
+
37
+ import { Aontu } from './aontu'
38
+ import type { TrustOptions } from './type'
39
+ import { vet } from './vet'
40
+ import type { VetFinding } from './vet'
41
+ import { get, why, evalFailure } from './query'
42
+ import { diff } from './diff'
43
+ import { canonHash, hcanon } from './hcanon'
44
+ import { cmpCodePoint } from './keyorder'
45
+ import { subsume } from './subsume'
46
+ import type { SubsumeVerdict } from './subsume'
47
+ import { trimCheck } from './trim'
48
+ import { jsonSchema } from './jsonschema'
49
+ import { relationCheck } from './relation'
50
+ import { reachCheck } from './reach'
51
+ import { patch } from './patch'
52
+
53
+
54
+ export const MCP_PROTOCOL = '2024-11-05'
55
+
56
+
57
+ export type McpRequest = {
58
+ id?: number | string | null
59
+ jsonrpc?: string
60
+ method?: string
61
+ params?: any
62
+ }
63
+
64
+ export type McpResponse = {
65
+ error?: { code: number, message: string }
66
+ id: number | string | null
67
+ jsonrpc: '2.0'
68
+ result?: any
69
+ }
70
+
71
+
72
+ // JSON-RPC's own codes, the three a server this small can raise.
73
+ const PARSE_ERROR = -32700
74
+ const METHOD_NOT_FOUND = -32601
75
+ const INVALID_PARAMS = -32602
76
+
77
+
78
+ export type ToolDef = {
79
+ name: string
80
+ description: string
81
+ properties: Record<
82
+ string, { type: string, description: string, items?: any }>
83
+ required: string[]
84
+ // The properties that carry DOCUMENT TEXT. Each one gains a
85
+ // `<name>Path` file alternative when the server runs with --root
86
+ // (toolList), and each one is what the confined pre-parse below
87
+ // covers for a tool that declares `refuse`.
88
+ docs?: string[]
89
+ // Argument validation beyond "required and a string": the message
90
+ // for a call that could not be made, or undefined for a good call.
91
+ check?: (a: any) => string | undefined
92
+ // ENGINES THAT TAKE NO TRUST PROFILE (subsume, trimCheck,
93
+ // relationCheck, patch) build their own evaluators, so the served
94
+ // profile cannot ride into them as an argument the way it rides
95
+ // into vet or get. Their documents are PRE-PARSED under the profile
96
+ // by callTool instead (confinedParseFailure below), and `refuse` is
97
+ // the tool's own error report for a document that fails it. A tool
98
+ // whose engine takes `trust` directly declares no `refuse`.
99
+ refuse?: (
100
+ a: any, finding: VetFinding, trust: TrustOptions,
101
+ paths: Record<string, string>) => any
102
+ run: (
103
+ a: any, trust: TrustOptions, paths: Record<string, string>) => any
104
+ }
105
+
106
+
107
+ // The trust profile a served evaluation runs under: no includes at
108
+ // all — or, when the server was started with --root, includes
109
+ // realpath-confined below that root (the CLI's `--trust root:<dir>`
110
+ // semantics; docs/trust.md). The package-resolver leg is enabled by
111
+ // neither.
112
+ //
113
+ // The profile is INJECTED into every tool by callTool rather than
114
+ // applied by each tool for itself. That is deliberate: four of the six
115
+ // original tools once called the library with no profile at all, so a
116
+ // served `@"x.js"` was require()d in the server process, while the
117
+ // module header claimed confinement. A tool that must remember to
118
+ // confine itself is a tool that eventually forgets, and the forgetting
119
+ // is silent. With the profile arriving as an argument, a tool cannot
120
+ // run unconfined without visibly discarding it.
121
+ export function servedTrust(root?: string): TrustOptions {
122
+ return null == root
123
+ ? { include: 'none' }
124
+ : { include: { root } }
125
+ }
126
+
127
+ function served(trust: TrustOptions): Aontu {
128
+ return new Aontu({ trust } as any)
129
+ }
130
+
131
+
132
+ // DOES THIS DOCUMENT STAND UP UNDER THE SERVED PROFILE — or the
133
+ // finding that says why not. This is the confinement gate for the
134
+ // engines that take no trust profile, and parse is the whole include
135
+ // story: `@"..."` resolves at parse time (ts/src/lang.ts), so a
136
+ // document whose confined parse is clean either has no includes at
137
+ // all (capability 'none') or resolves every one below the root — and
138
+ // an engine that then re-resolves the same closure under the default
139
+ // profile reads exactly the files the confined parse proved in
140
+ // bounds. A parse that fails for any reason refuses the call: an
141
+ // engine's own answer for a document this profile cannot read is not
142
+ // an answer this server may compute.
143
+ export function confinedParseFailure(
144
+ src: string, trust: TrustOptions, path?: string
145
+ ): VetFinding | undefined {
146
+ const aontu = served(trust)
147
+ const ctx = aontu.ctx({ collect: true })
148
+ aontu.parse(src, null == path ? undefined : { path }, ctx)
149
+ return 0 < ctx.err.length ? evalFailure(ctx) : undefined
150
+ }
151
+
152
+
153
+ // Confinement is realpath-then-prefix-check, mirroring the include
154
+ // resolver's own rule (ts/src/lang.ts, docs/trust.md): the file's
155
+ // real path must sit below the root's real path, so a symlink inside
156
+ // the root pointing outside it is an escape, not a loophole.
157
+ //
158
+ // A path that does not (fully) exist cannot be realpath'd whole, and
159
+ // falling back to the LEXICAL form compares apples to oranges when the
160
+ // root itself sits behind a symlink -- on macOS a root under /var
161
+ // realpaths to /private/var, so a merely-missing file inside it read
162
+ // as an escape instead of "cannot read" (the CI failure that bought
163
+ // this comment). Realpath the deepest EXISTING ancestor and re-attach
164
+ // the rest, so both sides of the prefix check are in real coordinates.
165
+ function realpathOf(p: string): string {
166
+ try {
167
+ return realpathSync(p)
168
+ }
169
+ catch {
170
+ const parent = pathDirname(p)
171
+ if (parent === p) {
172
+ return p
173
+ }
174
+ return pathJoin(realpathOf(parent), pathBasename(p))
175
+ }
176
+ }
177
+
178
+ function outsideRoot(root: string, full: string): boolean {
179
+ const rootReal = realpathOf(root)
180
+ const fullReal = realpathOf(full)
181
+ return fullReal !== rootReal && !fullReal.startsWith(rootReal + pathSep)
182
+ }
183
+
184
+
185
+ const TOOLS: ToolDef[] = [
186
+ {
187
+ name: 'vet',
188
+ description:
189
+ 'Validate a data document against a schema document. Returns the ' +
190
+ 'vet report: verdict (valid | invalid | incomplete | error), and ' +
191
+ 'findings with codes, paths, sites and a repair hint. A site ' +
192
+ 'names the file whose text it excerpts, so its row and column ' +
193
+ 'are safe to edit at even when the schema loads other files.',
194
+ properties: {
195
+ schema: { type: 'string', description: 'The schema document' },
196
+ data: { type: 'string', description: 'The data document' },
197
+ at: { type: 'string', description: 'Validate at this path ($.a.b)' },
198
+ },
199
+ required: ['schema', 'data'],
200
+ docs: ['schema', 'data'],
201
+ run: (a, trust, paths) => vet(str(a.schema), str(a.data), {
202
+ ...(null == a.at ? {} : { at: str(a.at) }),
203
+ schemaPath: paths.schema,
204
+ dataPath: paths.data,
205
+ schemaUrl: paths.schema,
206
+ dataUrl: paths.data,
207
+ trust,
208
+ }),
209
+ },
210
+ {
211
+ name: 'get',
212
+ description:
213
+ 'Select one node of an evaluated document by path and render it: ' +
214
+ 'generated JSON by default, or the canon, types, keys or ' +
215
+ 'depth-elided view. A view other than json is a valid Aontu ' +
216
+ 'document that subsumes the truth it summarises.',
217
+ properties: {
218
+ src: { type: 'string', description: 'The document' },
219
+ path: { type: 'string', description: 'The path ($.a.b)' },
220
+ view: {
221
+ type: 'string',
222
+ description: 'json (default), canon, types or keys',
223
+ },
224
+ depth: {
225
+ type: 'number',
226
+ description: 'Structure to this depth; deeper nodes render as top',
227
+ },
228
+ },
229
+ required: ['src', 'path'],
230
+ docs: ['src'],
231
+ run: (a, trust, paths) => get(str(a.src), str(a.path), {
232
+ view: a.view,
233
+ depth: 'number' === typeof a.depth ? a.depth : undefined,
234
+ path: paths.src,
235
+ trust,
236
+ }),
237
+ },
238
+ {
239
+ name: 'why',
240
+ description:
241
+ 'Explain the value at a path: every contribution that met there, ' +
242
+ 'in source order, with its role (literal, spread, ref, pref) and ' +
243
+ 'the site it was written at.',
244
+ properties: {
245
+ src: { type: 'string', description: 'The document' },
246
+ path: { type: 'string', description: 'The path ($.a.b)' },
247
+ },
248
+ required: ['src', 'path'],
249
+ docs: ['src'],
250
+ run: (a, trust, paths) =>
251
+ why(str(a.src), str(a.path), { path: paths.src, trust }),
252
+ },
253
+ {
254
+ name: 'diff',
255
+ description:
256
+ 'What changed, at which paths, between two documents. Compares ' +
257
+ 'the hash form, so reformatting is not a change and closing a ' +
258
+ 'map is. Whether a change is BREAKING is the breaking verb\'s ' +
259
+ 'question, not this one.',
260
+ properties: {
261
+ left: { type: 'string', description: 'The earlier document' },
262
+ right: { type: 'string', description: 'The later document' },
263
+ at: { type: 'string', description: 'Compare at this path ($.a.b)' },
264
+ },
265
+ required: ['left', 'right'],
266
+ docs: ['left', 'right'],
267
+ run: (a, trust, paths) => diff(str(a.left), str(a.right), {
268
+ ...(null == a.at ? {} : { at: str(a.at) }),
269
+ leftPath: paths.left,
270
+ rightPath: paths.right,
271
+ trust,
272
+ }),
273
+ },
274
+ {
275
+ name: 'canon',
276
+ description:
277
+ 'Normalise a document to its canonical form: the deterministic ' +
278
+ 'text two documents that mean the same thing share.',
279
+ properties: {
280
+ src: { type: 'string', description: 'The document' },
281
+ },
282
+ required: ['src'],
283
+ docs: ['src'],
284
+ run: (a, trust, paths) => canonOf(str(a.src), trust, paths.src),
285
+ },
286
+ {
287
+ name: 'summary',
288
+ description:
289
+ 'A document at a glance: its canon-hash pin, its root keys, and ' +
290
+ 'the shape of its top tier. The first tier of progressive ' +
291
+ 'disclosure — expand by calling get with a path.',
292
+ properties: {
293
+ src: { type: 'string', description: 'The document' },
294
+ },
295
+ required: ['src'],
296
+ docs: ['src'],
297
+ run: (a, trust, paths) => summaryOf(str(a.src), trust, paths.src),
298
+ },
299
+
300
+ // The evolution and change verbs (the use-case review's "MCP is a
301
+ // read-only subset" gap, use-cases/09-agent-tools/README.md gap 11).
302
+
303
+ {
304
+ name: 'subsume',
305
+ description:
306
+ 'Does the general document admit every instance the specific ' +
307
+ 'one admits? Returns the subsume report: verdict (subsumes | ' +
308
+ 'does_not_subsume | undecided | error) and compat findings, ' +
309
+ 'each with a witness at the path that narrowed.',
310
+ properties: {
311
+ general: { type: 'string', description: 'The general document' },
312
+ specific: { type: 'string', description: 'The specific document' },
313
+ profile: {
314
+ type: 'string',
315
+ description: 'values, defaults (default) or gen',
316
+ },
317
+ at: { type: 'string', description: 'Compare at this path ($.a.b)' },
318
+ },
319
+ required: ['general', 'specific'],
320
+ docs: ['general', 'specific'],
321
+ check: (a) => null == a.profile || 'values' === a.profile ||
322
+ 'defaults' === a.profile || 'gen' === a.profile
323
+ ? undefined : 'profile needs values, defaults or gen',
324
+ refuse: (_a, finding) => ({ verdict: 'error', findings: [finding] }),
325
+ run: (a, _trust, paths) => subsume(str(a.general), str(a.specific), {
326
+ ...(null == a.profile ? {} : { profile: a.profile }),
327
+ ...(null == a.at ? {} : { at: str(a.at) }),
328
+ generalUrl: paths.general,
329
+ specificUrl: paths.specific,
330
+ generalPath: paths.general,
331
+ specificPath: paths.specific,
332
+ }),
333
+ },
334
+ {
335
+ name: 'breaking',
336
+ description:
337
+ 'Is the new version of a document a breaking change against the ' +
338
+ 'old one? Wraps subsume in the CLI\'s policy logic: the mode ' +
339
+ 'argument, else the document\'s own $.aontu_policy.compat, else ' +
340
+ 'backward. Returns verdict (compatible | breaking | undecided | ' +
341
+ 'error), the mode checked, and the findings.',
342
+ properties: {
343
+ old: { type: 'string', description: 'The old (published) version' },
344
+ new: { type: 'string', description: 'The new (proposed) version' },
345
+ mode: {
346
+ type: 'string',
347
+ description: 'backward, forward or full; overrides the ' +
348
+ 'document\'s own $.aontu_policy.compat declaration',
349
+ },
350
+ },
351
+ required: ['old', 'new'],
352
+ docs: ['old', 'new'],
353
+ check: (a) => null == a.mode || 'backward' === a.mode ||
354
+ 'forward' === a.mode || 'full' === a.mode
355
+ ? undefined : 'mode needs backward, forward or full',
356
+ refuse: (a, finding, trust, paths) => ({
357
+ verdict: 'error',
358
+ mode: breakingMode(a, trust, paths),
359
+ findings: [finding],
360
+ }),
361
+ run: (a, trust, paths) => breakingOf(a, trust, paths),
362
+ },
363
+ {
364
+ name: 'set',
365
+ description:
366
+ 'Change values by appending to an overlay document (or, with ' +
367
+ 'inPlace, rewriting a pinned literal where that is provably ' +
368
+ 'safe). Returns the vet-class report plus the NEW OVERLAY TEXT: ' +
369
+ 'this server never writes files, so the caller owns the write.',
370
+ properties: {
371
+ entry: {
372
+ type: 'string',
373
+ description: 'The entry document (the truth the overlay must ' +
374
+ 'hold against)',
375
+ },
376
+ overlay: {
377
+ type: 'string',
378
+ description: 'The overlay document as it stands (may be empty)',
379
+ },
380
+ assignments: {
381
+ type: 'array',
382
+ items: {
383
+ type: 'object',
384
+ properties: {
385
+ path: { type: 'string', description: 'The path ($.a.b)' },
386
+ value: {
387
+ type: 'string',
388
+ description: 'The value, as Aontu source text',
389
+ },
390
+ },
391
+ required: ['path', 'value'],
392
+ },
393
+ description: 'The assignments to apply, in order',
394
+ },
395
+ inPlace: {
396
+ type: 'boolean',
397
+ description: 'Rewrite a pinned literal where it was written, ' +
398
+ 'where provably safe; otherwise append as usual',
399
+ },
400
+ },
401
+ required: ['entry', 'overlay', 'assignments'],
402
+ docs: ['entry', 'overlay'],
403
+ check: checkAssignments,
404
+ refuse: (a, finding) => setError(str(a.overlay), finding),
405
+ run: (a, trust, paths) => setOf(a, trust, paths),
406
+ },
407
+ {
408
+ name: 'relations',
409
+ description:
410
+ 'Check the declared relations of a finished model: acyclicity ' +
411
+ 'and inverse consistency over the entity edge set. Returns ' +
412
+ 'verdict (pass | fail | error) and relation findings.',
413
+ properties: {
414
+ source: { type: 'string', description: 'The document' },
415
+ },
416
+ required: ['source'],
417
+ docs: ['source'],
418
+ // The pre-parse finding rides `errors`, exactly where the engine
419
+ // puts its own reason for a document that does not stand up
420
+ // (ts/src/relation.ts, the review's finding F). NOT `findings`:
421
+ // RelationFinding is its own vocabulary (code, relation, at,
422
+ // detail) and a document with no graph has no graph findings.
423
+ refuse: (_a, finding) =>
424
+ ({ verdict: 'error', findings: [], errors: [finding] }),
425
+ run: (a, _trust, paths) =>
426
+ relationCheck(str(a.source), { path: paths.source }),
427
+ },
428
+ {
429
+ name: 'reaches',
430
+ description:
431
+ 'Ask whether one entity reaches another over the entity graph, ' +
432
+ 'at any remove — the closure question `relations` cannot ask one ' +
433
+ 'edge at a time (blast radius, containment). Returns verdict ' +
434
+ '(reaches | unreachable | error) and, when it reaches, a shortest ' +
435
+ 'path. Transitive, not reflexive: an entity reaches itself only ' +
436
+ 'through a cycle.',
437
+ properties: {
438
+ source: { type: 'string', description: 'The document' },
439
+ from: { type: 'string', description: 'The entity to start at' },
440
+ to: { type: 'string', description: 'The entity to look for' },
441
+ relation: {
442
+ type: 'string',
443
+ description: 'Follow only edges under this relation (optional)',
444
+ },
445
+ },
446
+ required: ['source', 'from', 'to'],
447
+ docs: ['source'],
448
+ refuse: (_a, finding) => ({ verdict: 'error', errors: [finding] }),
449
+ run: (a, _trust, paths) =>
450
+ reachCheck(str(a.source), str(a.from), str(a.to), {
451
+ path: paths.source,
452
+ relation: null == a.relation ? undefined : str(a.relation),
453
+ }),
454
+ },
455
+ {
456
+ name: 'hash',
457
+ description:
458
+ 'The canon-hash pin of a document: "aon1-" + ' +
459
+ 'base64url(SHA-256(hash form)). Survives reformatting; moves on ' +
460
+ 'any change of meaning. Pass form: true for the hash form text ' +
461
+ 'the pin digests.',
462
+ properties: {
463
+ source: { type: 'string', description: 'The document' },
464
+ form: {
465
+ type: 'boolean',
466
+ description: 'Include the hash form text as well',
467
+ },
468
+ },
469
+ required: ['source'],
470
+ docs: ['source'],
471
+ run: (a, trust, paths) =>
472
+ hashOf(str(a.source), true === a.form, trust, paths.source),
473
+ },
474
+ {
475
+ name: 'trim',
476
+ description:
477
+ 'Report redundant map entries — entries whose removal leaves ' +
478
+ 'the evaluated result unchanged, the spread-implied case ' +
479
+ 'included — as paths. Report-only, the CLI\'s trim --check. ' +
480
+ 'Returns verdict (clean | redundant | error).',
481
+ properties: {
482
+ source: { type: 'string', description: 'The document' },
483
+ },
484
+ required: ['source'],
485
+ docs: ['source'],
486
+ // The pre-parse finding rides `errors`, exactly where the engine
487
+ // puts its own reason for a document that does not stand up
488
+ // (ts/src/trim.ts, the review's finding F).
489
+ refuse: (_a, finding) =>
490
+ ({ verdict: 'error', redundant: [], errors: [finding] }),
491
+ run: (a, _trust, paths) =>
492
+ trimCheck(str(a.source), { path: paths.source }),
493
+ },
494
+ {
495
+ name: 'jsonschema',
496
+ description:
497
+ 'Export a document as a JSON Schema (draft 2020-12), and say ' +
498
+ 'what could not be carried. This is the bridge to a ' +
499
+ 'structured-output API, which constrains generation to JSON ' +
500
+ 'Schema and to nothing else -- and to an MCP tool\'s own ' +
501
+ 'inputSchema, which the protocol requires to be one. Returns ' +
502
+ 'verdict (ok | lossy | error), the schema, and a `lossy` list ' +
503
+ 'naming every construct the schema could not say and what it ' +
504
+ 'says instead. A lossy schema admits MORE than the model does, ' +
505
+ 'so vet the result against the model rather than trusting the ' +
506
+ 'schema alone.',
507
+ properties: {
508
+ source: { type: 'string', description: 'The document' },
509
+ at: {
510
+ type: 'string',
511
+ description: 'Export this path of the document ($.a.b)',
512
+ },
513
+ },
514
+ required: ['source'],
515
+ docs: ['source'],
516
+ refuse: (_a, finding) =>
517
+ ({ verdict: 'error', schema: {}, lossy: [], errors: [finding] }),
518
+ run: (a, _trust, paths) =>
519
+ jsonSchema(str(a.source), {
520
+ at: null == a.at ? undefined : str(a.at), path: paths.source,
521
+ }),
522
+ },
523
+ ]
524
+
525
+
526
+ function str(v: any): string {
527
+ return 'string' === typeof v ? v : ''
528
+ }
529
+
530
+
531
+ function canonOf(src: string, trust: TrustOptions, path?: string): any {
532
+ const aontu = served(trust)
533
+ const ctx = aontu.ctx({ collect: true })
534
+ const v: any = aontu.unify(
535
+ src, null == path ? undefined : { path }, ctx)
536
+ if (0 < ctx.err.length) {
537
+ return { ok: false, canon: '', findings: [evalFailure(ctx)] }
538
+ }
539
+ return { ok: true, canon: v.canon, findings: [] }
540
+ }
541
+
542
+
543
+ function summaryOf(src: string, trust: TrustOptions, path?: string): any {
544
+ const aontu = served(trust)
545
+ const ctx = aontu.ctx({ collect: true })
546
+ const v: any = aontu.unify(
547
+ src, null == path ? undefined : { path }, ctx)
548
+ if (0 < ctx.err.length) {
549
+ return {
550
+ ok: false, hash: '', keys: [], shape: '', findings: [evalFailure(ctx)],
551
+ }
552
+ }
553
+ const keys = true === v.isMap ? Object.keys(v.peg).sort(cmpCodePoint) : []
554
+ return {
555
+ ok: true,
556
+ hash: canonHash(v),
557
+ keys,
558
+ // The top tier only: every key, with its subtree elided to `top`.
559
+ shape: get(src, '$', { view: 'types', depth: 2, path, trust }).out,
560
+ findings: [],
561
+ }
562
+ }
563
+
564
+
565
+ // The pin, computed under the served profile — this engine is the
566
+ // evaluation itself, so the profile rides in directly and no pre-parse
567
+ // is needed. The error answer follows canonOf's shape; the CLI prints
568
+ // nothing but a message there (ts/src/cli.ts runHash), so this shape
569
+ // is the served superset of it.
570
+ function hashOf(
571
+ src: string, form: boolean, trust: TrustOptions, path?: string): any {
572
+ const aontu = served(trust)
573
+ const ctx = aontu.ctx({ collect: true })
574
+ const v: any = aontu.unify(
575
+ src, null == path ? undefined : { path }, ctx)
576
+ if (0 < ctx.err.length || null == v || true === v.isNil) {
577
+ return { ok: false, hash: '', findings: [evalFailure(ctx)] }
578
+ }
579
+ return {
580
+ ok: true,
581
+ hash: canonHash(v),
582
+ ...(form ? { form: hcanon(v) } : {}),
583
+ findings: [],
584
+ }
585
+ }
586
+
587
+
588
+ // ---------------------------------------------------------------------
589
+ // THE BREAKING POLICY WRAPPER. Re-implemented from ts/src/cli.ts
590
+ // (runBreaking and policyCompat) rather than extracted from it: cli.ts
591
+ // owns the process-facing halves — files, git revisions, exit codes,
592
+ // rendering — that a served tool must not import, and the policy
593
+ // itself (which mode applies, which side is general, how verdicts
594
+ // aggregate) is small enough that a shared home would be all seam.
595
+ // cli.ts is the reference; a change there is a change here.
596
+
597
+ type BreakingMode = 'backward' | 'forward' | 'full' | 'none'
598
+
599
+ // Verdict aggregation: an error anywhere makes the run an error;
600
+ // otherwise a witness anywhere makes it breaking; otherwise an open
601
+ // question anywhere leaves it undecided.
602
+ const BREAKING_RANK: Record<SubsumeVerdict, number> = {
603
+ subsumes: 0,
604
+ undecided: 1,
605
+ does_not_subsume: 2,
606
+ error: 3,
607
+ }
608
+
609
+ const BREAKING_VERDICT: Record<SubsumeVerdict, string> = {
610
+ subsumes: 'compatible',
611
+ does_not_subsume: 'breaking',
612
+ undecided: 'undecided',
613
+ error: 'error',
614
+ }
615
+
616
+
617
+ // The document's own compatibility declaration: `$.aontu_policy.compat`,
618
+ // a disjunction whose default is the declared mode. Undefined when the
619
+ // key is absent or does not spell a mode. The one departure from the
620
+ // cli.ts original: the read runs CONFINED, because here the document
621
+ // came from a caller.
622
+ function policyCompatOf(
623
+ newSrc: string, trust: TrustOptions, path?: string
624
+ ): BreakingMode | undefined {
625
+ const aontu = served(trust)
626
+ const ctx = aontu.ctx({ collect: true })
627
+ const v: any = aontu.unify(
628
+ newSrc, null == path ? undefined : { path }, ctx)
629
+ if (0 < ctx.err.length || true === v?.isNil) {
630
+ return undefined
631
+ }
632
+ let compat: any = v?.peg?.aontu_policy?.peg?.compat
633
+ if (null == compat) {
634
+ return undefined
635
+ }
636
+ if (true === compat.isDisjunct && Array.isArray(compat.peg)) {
637
+ compat = compat.peg.find((m: any) => true === m?.isPref) ?? compat.peg[0]
638
+ }
639
+ if (true === compat.isPref) {
640
+ compat = compat.peg
641
+ }
642
+ const m = true === compat?.isString ? compat.peg : undefined
643
+ return 'backward' === m || 'forward' === m || 'full' === m || 'none' === m
644
+ ? m : undefined
645
+ }
646
+
647
+
648
+ // The declared mode: the mode argument overrides the document's own
649
+ // policy; neither means backward (v1-valid documents stay valid).
650
+ function breakingMode(
651
+ a: any, trust: TrustOptions, paths: Record<string, string>
652
+ ): BreakingMode {
653
+ return a.mode ?? policyCompatOf(str(a.new), trust, paths.new) ?? 'backward'
654
+ }
655
+
656
+
657
+ function breakingOf(
658
+ a: any, trust: TrustOptions, paths: Record<string, string>): any {
659
+ const mode = breakingMode(a, trust, paths)
660
+
661
+ if ('none' === mode) {
662
+ // The document declares no compatibility promise: nothing to check.
663
+ return { verdict: 'compatible', mode, findings: [] }
664
+ }
665
+
666
+ const sides = {
667
+ old: { src: str(a.old), url: paths.old ?? 'old', path: paths.old },
668
+ new: { src: str(a.new), url: paths.new ?? 'new', path: paths.new },
669
+ }
670
+
671
+ // backward: the NEW document is the general side — every old
672
+ // instance must still be admitted. forward: the old one is.
673
+ const checks: Array<{
674
+ general: typeof sides.old, specific: typeof sides.old
675
+ }> = []
676
+ if ('backward' === mode || 'full' === mode) {
677
+ checks.push({ general: sides.new, specific: sides.old })
678
+ }
679
+ if ('forward' === mode || 'full' === mode) {
680
+ checks.push({ general: sides.old, specific: sides.new })
681
+ }
682
+
683
+ let worst: SubsumeVerdict = 'subsumes'
684
+ const findings: VetFinding[] = []
685
+ for (const check of checks) {
686
+ const report = subsume(check.general.src, check.specific.src, {
687
+ generalUrl: check.general.url,
688
+ specificUrl: check.specific.url,
689
+ generalPath: check.general.path,
690
+ specificPath: check.specific.path,
691
+ })
692
+ if (BREAKING_RANK[worst] < BREAKING_RANK[report.verdict]) {
693
+ worst = report.verdict
694
+ }
695
+ findings.push(...report.findings)
696
+ }
697
+
698
+ return { verdict: BREAKING_VERDICT[worst], mode, findings }
699
+ }
700
+
701
+
702
+ // ---------------------------------------------------------------------
703
+ // The set tool: the CLI's `set` verb minus the filesystem — the patch
704
+ // engine (ts/src/patch.ts) already answers with the new overlay text,
705
+ // and the caller owns the write.
706
+
707
+ // The assignments arrive structured ({path, value}) rather than as the
708
+ // CLI's `<path>=<value>` spelling, and are re-joined for the engine's
709
+ // parseAssignment — so the path must not smuggle a `=` that would move
710
+ // the split.
711
+ function checkAssignments(a: any): string | undefined {
712
+ if (0 === a.assignments.length) {
713
+ return 'assignments needs at least one {path, value}'
714
+ }
715
+ for (const x of a.assignments) {
716
+ if ('string' !== typeof x?.path || '' === x.path.trim() ||
717
+ 'string' !== typeof x?.value || '' === x.value.trim()) {
718
+ return 'each assignment needs a path and a value, both strings'
719
+ }
720
+ if (x.path.includes('=')) {
721
+ return `assignment path may not contain "=": ${x.path}`
722
+ }
723
+ }
724
+ return undefined
725
+ }
726
+
727
+
728
+ function setError(overlay: string, finding: VetFinding): any {
729
+ return {
730
+ overlay,
731
+ appended: [],
732
+ replaced: [],
733
+ verdict: 'error',
734
+ findings: [finding],
735
+ }
736
+ }
737
+
738
+
739
+ function setOf(
740
+ a: any, trust: TrustOptions, paths: Record<string, string>): any {
741
+ // THE ASSIGNMENT VALUES ARE DOCUMENTS TOO: each one is appended (or
742
+ // spliced) into the overlay and evaluated there by the engine's
743
+ // final vet, so a value that smuggles an include — or a newline and
744
+ // then an include — gets the same confined pre-parse as the
745
+ // documents themselves, wrapped exactly as the engine's own
746
+ // spanValue wraps a fragment (ts/src/patch.ts).
747
+ for (const x of a.assignments) {
748
+ const denied = confinedParseFailure('v: ' + x.value, trust, paths.overlay)
749
+ if (null != denied) {
750
+ return setError(str(a.overlay), denied)
751
+ }
752
+ }
753
+
754
+ return patch(str(a.entry), str(a.overlay),
755
+ a.assignments.map((x: any) => x.path + '=' + x.value),
756
+ {
757
+ entryPath: paths.entry,
758
+ overlayPath: paths.overlay,
759
+ inPlace: true === a.inPlace,
760
+ })
761
+ }
762
+
763
+
764
+ // ---------------------------------------------------------------------
765
+ // The tool list as MCP spells it: a name, a description, and a JSON
766
+ // Schema for the arguments. With a served root, every document
767
+ // property gains its `<name>Path` file alternative — and comes OFF the
768
+ // `required` list, because JSON Schema's `required` cannot say "one of
769
+ // the two"; callTool's own argument check still refuses a call that
770
+ // carries neither.
771
+ export function toolList(root?: string): any[] {
772
+ return TOOLS.map((t) => {
773
+ const properties: Record<string, any> = { ...t.properties }
774
+ let required = t.required
775
+ if (null != root && null != t.docs) {
776
+ for (const doc of t.docs) {
777
+ properties[doc + 'Path'] = {
778
+ type: 'string',
779
+ description: `File below the server's --root, read as ` +
780
+ `\`${doc}\`; an alternative to inline \`${doc}\` text`,
781
+ }
782
+ }
783
+ required = t.required.filter(
784
+ (r) => !(t.docs as string[]).includes(r))
785
+ }
786
+ return {
787
+ name: t.name,
788
+ description: t.description,
789
+ inputSchema: {
790
+ type: 'object',
791
+ properties,
792
+ required,
793
+ },
794
+ }
795
+ })
796
+ }
797
+
798
+
799
+ function refusal(text: string): any {
800
+ return { content: [{ type: 'text', text }], isError: true }
801
+ }
802
+
803
+
804
+ // One tool call. A tool that REFUSES (an invalid document, a path that
805
+ // names nothing, a document the served profile cannot read) is not a
806
+ // protocol error: it answers with its own report and `isError` false,
807
+ // because the report IS the answer the agent asked for. `isError` is
808
+ // reserved for a call that could not be made at all — an unknown tool,
809
+ // a missing or malformed argument, a file argument the server cannot
810
+ // serve.
811
+ // `opts.tools` is injectable for the same reason the watch loop's
812
+ // waiter is: the catch below is defensive code no document reaches --
813
+ // every verb answers with a report rather than throwing -- and code
814
+ // the suite cannot execute is code the ADR-002 floor cannot hold.
815
+ export function callTool(
816
+ name: string, args: any,
817
+ opts?: { root?: string, tools?: ToolDef[] }): any {
818
+ const root = opts?.root
819
+ const tools = opts?.tools ?? TOOLS
820
+ const tool = tools.find((t) => t.name === name)
821
+ if (null == tool) {
822
+ return refusal(`no such tool: ${name}`)
823
+ }
824
+
825
+ const a: any = { ...(args ?? {}) }
826
+ const paths: Record<string, string> = {}
827
+
828
+ // THE FILE ALTERNATIVES (--root). A document that did not arrive as
829
+ // inline text may arrive as a `<name>Path` file — served only when
830
+ // the operator granted a root at startup, confined below it by the
831
+ // same realpath rule the include resolver applies, and recorded in
832
+ // `paths` so the engine resolves the file's own relative includes
833
+ // from its directory (the CLI's rule for a named file). Inline text
834
+ // wins when a caller sends both.
835
+ for (const doc of tool.docs ?? []) {
836
+ if ('string' === typeof a[doc]) {
837
+ continue
838
+ }
839
+ const rel = a[doc + 'Path']
840
+ if ('string' !== typeof rel) {
841
+ continue
842
+ }
843
+ if (null == root) {
844
+ return refusal(
845
+ `tool ${name}: ${doc}Path needs a server started with ` +
846
+ `--root <dir>; pass ${doc} as document text instead`)
847
+ }
848
+ const full = pathResolve(root, rel)
849
+ if (outsideRoot(root, full)) {
850
+ return refusal(
851
+ `tool ${name}: ${doc}Path escapes the server root: ${rel}`)
852
+ }
853
+ try {
854
+ a[doc] = readFileSync(full, 'utf8')
855
+ }
856
+ catch (e: any) {
857
+ return refusal(
858
+ `tool ${name}: cannot read ${doc}Path ${rel}: ${e?.message ?? e}`)
859
+ }
860
+ paths[doc] = full
861
+ }
862
+
863
+ for (const req of tool.required) {
864
+ const kind = tool.properties[req]?.type
865
+ const okv = 'array' === kind
866
+ ? Array.isArray(a[req])
867
+ : 'string' === typeof a[req]
868
+ if (!okv) {
869
+ const alt = null != root && (tool.docs ?? []).includes(req)
870
+ ? ` (or ${req}Path)` : ''
871
+ return refusal(
872
+ `tool ${name} needs ${'array' === kind ? 'an array' : 'a string'}` +
873
+ ` argument: ${req}${alt}`)
874
+ }
875
+ }
876
+
877
+ const bad = null == tool.check ? undefined : tool.check(a)
878
+ if (null != bad) {
879
+ return refusal(`tool ${name}: ${bad}`)
880
+ }
881
+
882
+ // The served profile is supplied HERE, once, for every tool.
883
+ // A tool never chooses its own confinement.
884
+ const trust = servedTrust(root)
885
+ let out: any
886
+ try {
887
+ // The confined pre-parse, for the engines that cannot take the
888
+ // profile themselves (see ToolDef.refuse).
889
+ if (null != tool.refuse) {
890
+ for (const doc of tool.docs ?? []) {
891
+ if ('string' !== typeof a[doc]) {
892
+ continue
893
+ }
894
+ const denied = confinedParseFailure(a[doc], trust, paths[doc])
895
+ if (null != denied) {
896
+ out = tool.refuse(a, denied, trust, paths)
897
+ break
898
+ }
899
+ }
900
+ }
901
+ if (undefined === out) {
902
+ out = tool.run(a, trust, paths)
903
+ }
904
+ }
905
+ catch (e: any) {
906
+ // A tool that throws is a call that could not be made, which is
907
+ // what isError means -- and it must not take the server process
908
+ // down with it: a stdio server serves one client for a whole
909
+ // session, so an unhandled throw on one document loses every
910
+ // later call too.
911
+ return refusal(`tool ${name} failed: ${e?.message ?? e}`)
912
+ }
913
+ return {
914
+ content: [{ type: 'text', text: JSON.stringify(out, null, 2) }],
915
+ isError: false,
916
+ }
917
+ }
918
+
919
+
920
+ // What a connecting client is told once, at the handshake: which
921
+ // confinement mode this server is in. The --root capability is a
922
+ // startup grant, not a per-call negotiation, so initialize is where a
923
+ // caller learns whether `<name>Path` arguments are served.
924
+ export function serverInstructions(root?: string): string {
925
+ return 'Aontu tools take document TEXT arguments and answer the ' +
926
+ 'same JSON reports the aontu CLI prints with --format json. ' +
927
+ (null == root
928
+ ? 'Evaluation is confined: includes (@"...") are denied, and ' +
929
+ 'file-path arguments (schemaPath, srcPath, sourcePath, ...) ' +
930
+ 'are refused. Start the server with --root <dir> to serve ' +
931
+ 'both, confined below that directory.'
932
+ : `This server was started with --root ${root}: every document ` +
933
+ 'argument also accepts a <name>Path alternative naming a file ' +
934
+ 'below that root, and includes (@"...") resolve confined to it.')
935
+ }
936
+
937
+
938
+ // Handle one JSON-RPC message. Returns undefined for a NOTIFICATION
939
+ // (no id): MCP sends `notifications/initialized`, and answering a
940
+ // notification is a protocol error in the other direction.
941
+ export function handle(
942
+ msg: McpRequest, version: string, root?: string
943
+ ): McpResponse | undefined {
944
+ const id = msg.id ?? null
945
+
946
+ if (null == msg.id) {
947
+ return undefined
948
+ }
949
+
950
+ switch (msg.method) {
951
+ case 'initialize':
952
+ return ok(id, {
953
+ protocolVersion: MCP_PROTOCOL,
954
+ capabilities: { tools: {} },
955
+ serverInfo: { name: 'aontu', version },
956
+ instructions: serverInstructions(root),
957
+ })
958
+
959
+ case 'ping':
960
+ return ok(id, {})
961
+
962
+ case 'tools/list':
963
+ return ok(id, { tools: toolList(root) })
964
+
965
+ case 'tools/call': {
966
+ const name = msg.params?.name
967
+ if ('string' !== typeof name) {
968
+ return err(id, INVALID_PARAMS, 'tools/call needs a tool name')
969
+ }
970
+ return ok(id, callTool(name, msg.params?.arguments ?? {}, { root }))
971
+ }
972
+
973
+ default:
974
+ return err(id, METHOD_NOT_FOUND, `no such method: ${msg.method}`)
975
+ }
976
+ }
977
+
978
+
979
+ // A message that did not decode at all.
980
+ export function parseError(): McpResponse {
981
+ return err(null, PARSE_ERROR, 'invalid JSON')
982
+ }
983
+
984
+
985
+ function ok(id: number | string | null, result: any): McpResponse {
986
+ return { jsonrpc: '2.0', id, result }
987
+ }
988
+
989
+
990
+ function err(
991
+ id: number | string | null, code: number, message: string): McpResponse {
992
+ return { jsonrpc: '2.0', id, error: { code, message } }
993
+ }