@intentius/chant 0.49.0 → 0.50.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 (247) hide show
  1. package/dist/audit/catalog.d.ts +13 -3
  2. package/dist/audit/catalog.d.ts.map +1 -1
  3. package/dist/audit/core.d.ts +9 -0
  4. package/dist/audit/core.d.ts.map +1 -1
  5. package/dist/audit/discover.d.ts +6 -0
  6. package/dist/audit/discover.d.ts.map +1 -1
  7. package/dist/audit/fetch.d.ts.map +1 -1
  8. package/dist/audit/report-html.d.ts.map +1 -1
  9. package/dist/audit/report-model.d.ts +6 -0
  10. package/dist/audit/report-model.d.ts.map +1 -1
  11. package/dist/audit/report.d.ts.map +1 -1
  12. package/dist/audit/rules-doc.d.ts.map +1 -1
  13. package/dist/audit/secrets.d.ts +95 -0
  14. package/dist/audit/secrets.d.ts.map +1 -0
  15. package/dist/audit/wrangler.d.ts +33 -0
  16. package/dist/audit/wrangler.d.ts.map +1 -0
  17. package/dist/build.d.ts.map +1 -1
  18. package/dist/cli/commands/audit.d.ts +7 -0
  19. package/dist/cli/commands/audit.d.ts.map +1 -1
  20. package/dist/cli/commands/build.d.ts +23 -0
  21. package/dist/cli/commands/build.d.ts.map +1 -1
  22. package/dist/cli/handlers/build.d.ts.map +1 -1
  23. package/dist/cli/handlers/components.d.ts +31 -0
  24. package/dist/cli/handlers/components.d.ts.map +1 -1
  25. package/dist/cli/handlers/lifecycle.d.ts +11 -0
  26. package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
  27. package/dist/cli/handlers/operator.d.ts +32 -0
  28. package/dist/cli/handlers/operator.d.ts.map +1 -0
  29. package/dist/cli/handlers/scenario.d.ts +39 -0
  30. package/dist/cli/handlers/scenario.d.ts.map +1 -0
  31. package/dist/cli/main.d.ts.map +1 -1
  32. package/dist/cli/mcp/server.d.ts +35 -2
  33. package/dist/cli/mcp/server.d.ts.map +1 -1
  34. package/dist/cli/mcp/types.d.ts +29 -1
  35. package/dist/cli/mcp/types.d.ts.map +1 -1
  36. package/dist/cli/registry.d.ts +14 -2
  37. package/dist/cli/registry.d.ts.map +1 -1
  38. package/dist/codegen/docs-rule-scanning.d.ts.map +1 -1
  39. package/dist/components/capability.d.ts +17 -2
  40. package/dist/components/capability.d.ts.map +1 -1
  41. package/dist/components/cli-support.d.ts +7 -0
  42. package/dist/components/cli-support.d.ts.map +1 -1
  43. package/dist/components/component.d.ts +15 -0
  44. package/dist/components/component.d.ts.map +1 -1
  45. package/dist/components/driver.d.ts.map +1 -1
  46. package/dist/components/verbs/index.d.ts +6 -1
  47. package/dist/components/verbs/index.d.ts.map +1 -1
  48. package/dist/components/verbs/run-agent.d.ts +499 -0
  49. package/dist/components/verbs/run-agent.d.ts.map +1 -0
  50. package/dist/components/verbs/sign.d.ts +30 -0
  51. package/dist/components/verbs/sign.d.ts.map +1 -1
  52. package/dist/composite.d.ts +6 -1
  53. package/dist/composite.d.ts.map +1 -1
  54. package/dist/discovery/collect.d.ts.map +1 -1
  55. package/dist/discovery/fold-import.d.ts +15 -1
  56. package/dist/discovery/fold-import.d.ts.map +1 -1
  57. package/dist/discovery/fold-rank.d.ts +66 -0
  58. package/dist/discovery/fold-rank.d.ts.map +1 -0
  59. package/dist/discovery/index.d.ts +15 -0
  60. package/dist/discovery/index.d.ts.map +1 -1
  61. package/dist/discovery/param-deps.d.ts +17 -0
  62. package/dist/discovery/param-deps.d.ts.map +1 -0
  63. package/dist/fold/fold.d.ts +55 -2
  64. package/dist/fold/fold.d.ts.map +1 -1
  65. package/dist/fold/subset.d.ts +21 -14
  66. package/dist/fold/subset.d.ts.map +1 -1
  67. package/dist/lexicon-schema.d.ts +2 -0
  68. package/dist/lexicon-schema.d.ts.map +1 -1
  69. package/dist/lexicon.d.ts +93 -0
  70. package/dist/lexicon.d.ts.map +1 -1
  71. package/dist/lifecycle/converge-ledger.d.ts +90 -0
  72. package/dist/lifecycle/converge-ledger.d.ts.map +1 -0
  73. package/dist/lifecycle/deep-diff.d.ts +18 -0
  74. package/dist/lifecycle/deep-diff.d.ts.map +1 -1
  75. package/dist/lifecycle/deep-observe.d.ts +9 -1
  76. package/dist/lifecycle/deep-observe.d.ts.map +1 -1
  77. package/dist/lifecycle/gate-ledger.d.ts +33 -0
  78. package/dist/lifecycle/gate-ledger.d.ts.map +1 -0
  79. package/dist/lifecycle/git.d.ts +145 -21
  80. package/dist/lifecycle/git.d.ts.map +1 -1
  81. package/dist/lifecycle/index.d.ts +4 -0
  82. package/dist/lifecycle/index.d.ts.map +1 -1
  83. package/dist/lifecycle/lease.d.ts +113 -0
  84. package/dist/lifecycle/lease.d.ts.map +1 -0
  85. package/dist/lifecycle/scenario-eval.d.ts +42 -0
  86. package/dist/lifecycle/scenario-eval.d.ts.map +1 -0
  87. package/dist/lifecycle/scenario.d.ts +163 -0
  88. package/dist/lifecycle/scenario.d.ts.map +1 -0
  89. package/dist/lifecycle/symptoms.d.ts +63 -0
  90. package/dist/lifecycle/symptoms.d.ts.map +1 -0
  91. package/dist/lint/output-docs.d.ts +94 -0
  92. package/dist/lint/output-docs.d.ts.map +1 -0
  93. package/dist/lint/post-synth.d.ts +29 -0
  94. package/dist/lint/post-synth.d.ts.map +1 -1
  95. package/dist/lint/rules/__fixtures__/comp/comp003/pass/agent-turn.component.d.ts +11 -0
  96. package/dist/lint/rules/__fixtures__/comp/comp003/pass/agent-turn.component.d.ts.map +1 -0
  97. package/dist/lsp/lexicon-providers.d.ts +7 -0
  98. package/dist/lsp/lexicon-providers.d.ts.map +1 -1
  99. package/dist/op/activity-contract.d.ts +139 -0
  100. package/dist/op/activity-contract.d.ts.map +1 -0
  101. package/dist/op/builders.d.ts +42 -2
  102. package/dist/op/builders.d.ts.map +1 -1
  103. package/dist/op/converge-rule.d.ts +161 -0
  104. package/dist/op/converge-rule.d.ts.map +1 -0
  105. package/dist/op/generate-pipeline.d.ts +39 -0
  106. package/dist/op/generate-pipeline.d.ts.map +1 -0
  107. package/dist/op/index.d.ts +14 -0
  108. package/dist/op/index.d.ts.map +1 -1
  109. package/dist/op/local-executor.d.ts.map +1 -1
  110. package/dist/op/op-verb-class.d.ts +42 -0
  111. package/dist/op/op-verb-class.d.ts.map +1 -0
  112. package/dist/op/operator.d.ts +128 -0
  113. package/dist/op/operator.d.ts.map +1 -0
  114. package/dist/op/step-output-ref.d.ts +187 -0
  115. package/dist/op/step-output-ref.d.ts.map +1 -0
  116. package/dist/op/types.d.ts +18 -1
  117. package/dist/op/types.d.ts.map +1 -1
  118. package/dist/provenance.d.ts +73 -3
  119. package/dist/provenance.d.ts.map +1 -1
  120. package/dist/runtime-adapter.d.ts +7 -1
  121. package/dist/runtime-adapter.d.ts.map +1 -1
  122. package/dist/serializer.d.ts +18 -0
  123. package/dist/serializer.d.ts.map +1 -1
  124. package/dist/toml.d.ts +40 -5
  125. package/dist/toml.d.ts.map +1 -1
  126. package/package.json +1 -1
  127. package/src/audit/catalog.test.ts +1 -1
  128. package/src/audit/catalog.ts +75 -3
  129. package/src/audit/core.ts +9 -0
  130. package/src/audit/discover.ts +29 -2
  131. package/src/audit/fetch.test.ts +216 -3
  132. package/src/audit/fetch.ts +270 -59
  133. package/src/audit/report-html.ts +5 -2
  134. package/src/audit/report-model.ts +9 -0
  135. package/src/audit/report.test.ts +22 -0
  136. package/src/audit/report.ts +3 -2
  137. package/src/audit/rules-doc.ts +2 -0
  138. package/src/audit/secrets.test.ts +303 -0
  139. package/src/audit/secrets.ts +406 -0
  140. package/src/audit/wrangler.test.ts +230 -0
  141. package/src/audit/wrangler.ts +290 -0
  142. package/src/build.ts +8 -3
  143. package/src/cli/command-group.ts +1 -1
  144. package/src/cli/commands/__fixtures__/schemas/sarif-2.1.0.schema.json +2882 -0
  145. package/src/cli/commands/audit.test.ts +215 -1
  146. package/src/cli/commands/audit.ts +86 -17
  147. package/src/cli/commands/build.test.ts +167 -2
  148. package/src/cli/commands/build.ts +114 -23
  149. package/src/cli/handlers/build.ts +2 -0
  150. package/src/cli/handlers/components.test.ts +199 -1
  151. package/src/cli/handlers/components.ts +160 -3
  152. package/src/cli/handlers/graph.test.ts +20 -0
  153. package/src/cli/handlers/graph.ts +10 -1
  154. package/src/cli/handlers/lifecycle.ts +12 -4
  155. package/src/cli/handlers/operator.test.ts +255 -0
  156. package/src/cli/handlers/operator.ts +240 -0
  157. package/src/cli/handlers/scenario.test.ts +456 -0
  158. package/src/cli/handlers/scenario.ts +330 -0
  159. package/src/cli/main.test.ts +23 -0
  160. package/src/cli/main.ts +72 -1
  161. package/src/cli/mcp/server.test.ts +265 -2
  162. package/src/cli/mcp/server.ts +84 -7
  163. package/src/cli/mcp/types.ts +27 -1
  164. package/src/cli/registry.ts +14 -2
  165. package/src/codegen/docs-rule-scanning.test.ts +42 -0
  166. package/src/codegen/docs-rule-scanning.ts +25 -2
  167. package/src/components/README.md +7 -0
  168. package/src/components/capability.ts +17 -2
  169. package/src/components/cli-support.test.ts +17 -0
  170. package/src/components/cli-support.ts +13 -1
  171. package/src/components/component-schema.test.ts +32 -0
  172. package/src/components/component.schema.json +6 -0
  173. package/src/components/component.test.ts +21 -0
  174. package/src/components/component.ts +15 -0
  175. package/src/components/driver.ts +12 -4
  176. package/src/components/verbs/index.ts +6 -1
  177. package/src/components/verbs/run-agent.test.ts +683 -0
  178. package/src/components/verbs/run-agent.ts +786 -0
  179. package/src/components/verbs/sign.test.ts +19 -0
  180. package/src/components/verbs/sign.ts +34 -2
  181. package/src/composite.ts +31 -2
  182. package/src/discovery/collect.ts +11 -2
  183. package/src/discovery/fold-import.test.ts +54 -0
  184. package/src/discovery/fold-import.ts +178 -38
  185. package/src/discovery/fold-rank.test.ts +197 -0
  186. package/src/discovery/fold-rank.ts +346 -0
  187. package/src/discovery/index.ts +16 -1
  188. package/src/discovery/param-deps.test.ts +118 -0
  189. package/src/discovery/param-deps.ts +170 -0
  190. package/src/fold/fold.test.ts +6 -2
  191. package/src/fold/fold.ts +184 -3
  192. package/src/fold/subset.test.ts +82 -19
  193. package/src/fold/subset.ts +79 -41
  194. package/src/lexicon-schema.ts +3 -0
  195. package/src/lexicon.ts +103 -2
  196. package/src/lifecycle/converge-ledger.test.ts +199 -0
  197. package/src/lifecycle/converge-ledger.ts +179 -0
  198. package/src/lifecycle/deep-diff.test.ts +79 -1
  199. package/src/lifecycle/deep-diff.ts +23 -0
  200. package/src/lifecycle/deep-observe.ts +13 -2
  201. package/src/lifecycle/gate-ledger.test.ts +103 -0
  202. package/src/lifecycle/gate-ledger.ts +140 -0
  203. package/src/lifecycle/git.test.ts +430 -0
  204. package/src/lifecycle/git.ts +446 -84
  205. package/src/lifecycle/index.ts +4 -0
  206. package/src/lifecycle/lease.test.ts +343 -0
  207. package/src/lifecycle/lease.ts +270 -0
  208. package/src/lifecycle/scenario-eval.test.ts +199 -0
  209. package/src/lifecycle/scenario-eval.ts +158 -0
  210. package/src/lifecycle/scenario.test.ts +195 -0
  211. package/src/lifecycle/scenario.ts +321 -0
  212. package/src/lifecycle/symptoms.test.ts +116 -0
  213. package/src/lifecycle/symptoms.ts +126 -0
  214. package/src/lint/output-docs.test.ts +220 -0
  215. package/src/lint/output-docs.ts +204 -0
  216. package/src/lint/post-synth.test.ts +97 -0
  217. package/src/lint/post-synth.ts +45 -0
  218. package/src/lint/rules/__fixtures__/comp/comp003/pass/agent-turn.component.ts +26 -0
  219. package/src/lint/rules/comp/comp.test.ts +49 -1
  220. package/src/lint/rules/evl001-non-literal-expression.test.ts +8 -3
  221. package/src/lint/rules/evl001-non-literal-expression.ts +6 -6
  222. package/src/lsp/lexicon-providers.test.ts +44 -0
  223. package/src/lsp/lexicon-providers.ts +11 -1
  224. package/src/op/activity-contract.test.ts +180 -0
  225. package/src/op/activity-contract.ts +278 -0
  226. package/src/op/builders-exports.test.ts +17 -1
  227. package/src/op/builders.ts +59 -5
  228. package/src/op/converge-rule.test.ts +179 -0
  229. package/src/op/converge-rule.ts +311 -0
  230. package/src/op/generate-pipeline.test.ts +53 -0
  231. package/src/op/generate-pipeline.ts +99 -0
  232. package/src/op/index.ts +30 -0
  233. package/src/op/local-executor.test.ts +92 -0
  234. package/src/op/local-executor.ts +45 -9
  235. package/src/op/op-verb-class.test.ts +126 -0
  236. package/src/op/op-verb-class.ts +115 -0
  237. package/src/op/operator.test.ts +346 -0
  238. package/src/op/operator.ts +213 -0
  239. package/src/op/step-output-ref.test.ts +334 -0
  240. package/src/op/step-output-ref.ts +453 -0
  241. package/src/op/types.ts +18 -1
  242. package/src/provenance.test.ts +151 -4
  243. package/src/provenance.ts +118 -4
  244. package/src/runtime-adapter.ts +31 -10
  245. package/src/serializer.ts +18 -0
  246. package/src/toml.test.ts +157 -384
  247. package/src/toml.ts +371 -5
@@ -47,7 +47,8 @@ describe("McpServer", () => {
47
47
 
48
48
  expect(response.error).toBeUndefined();
49
49
  const result = response.result as Record<string, unknown>;
50
- expect(result.protocolVersion).toBe("2024-11-05");
50
+ // No requested version → negotiates to the latest supported revision (#1194).
51
+ expect(result.protocolVersion).toBe("2026-07-28");
51
52
  expect(result.capabilities).toBeDefined();
52
53
  expect((result.serverInfo as Record<string, unknown>).name).toBe("chant");
53
54
  expect((result.serverInfo as Record<string, unknown>).version).toBe("0.1.0");
@@ -60,11 +61,273 @@ describe("McpServer", () => {
60
61
  method: "initialize",
61
62
  params: {},
62
63
  });
63
- const result = response.result as Record<string, unknown>;
64
+ const result = response!.result as Record<string, unknown>;
64
65
  const caps = result.capabilities as Record<string, unknown>;
65
66
  expect(caps.tools).toBeDefined();
66
67
  expect(caps.resources).toBeDefined();
67
68
  });
69
+
70
+ // -----------------------------------------------------------------------
71
+ // Dual-revision negotiation (#1194)
72
+ // -----------------------------------------------------------------------
73
+
74
+ test("a 2024-11-05 client requesting its own version gets it back unchanged", async () => {
75
+ const response = await server.handleRequest({
76
+ jsonrpc: "2.0",
77
+ id: 1,
78
+ method: "initialize",
79
+ params: { protocolVersion: "2024-11-05", clientInfo: { name: "old-client", version: "1.0" } },
80
+ });
81
+ expect(response!.error).toBeUndefined();
82
+ const result = response!.result as Record<string, unknown>;
83
+ expect(result.protocolVersion).toBe("2024-11-05");
84
+ });
85
+
86
+ test("a 2026-07-28 client requesting its version gets the latest revision", async () => {
87
+ const response = await server.handleRequest({
88
+ jsonrpc: "2.0",
89
+ id: 1,
90
+ method: "initialize",
91
+ params: { _meta: { protocolVersion: "2026-07-28" } },
92
+ });
93
+ expect(response!.error).toBeUndefined();
94
+ const result = response!.result as Record<string, unknown>;
95
+ expect(result.protocolVersion).toBe("2026-07-28");
96
+ });
97
+
98
+ test("an unrecognized requested version falls back to the latest supported", async () => {
99
+ const response = await server.handleRequest({
100
+ jsonrpc: "2.0",
101
+ id: 1,
102
+ method: "initialize",
103
+ params: { protocolVersion: "1999-01-01" },
104
+ });
105
+ const result = response!.result as Record<string, unknown>;
106
+ expect(result.protocolVersion).toBe("2026-07-28");
107
+ });
108
+
109
+ test("no requested version at all defaults to the latest supported", async () => {
110
+ const response = await server.handleRequest({
111
+ jsonrpc: "2.0",
112
+ id: 1,
113
+ method: "initialize",
114
+ params: {},
115
+ });
116
+ const result = response!.result as Record<string, unknown>;
117
+ expect(result.protocolVersion).toBe("2026-07-28");
118
+ });
119
+ });
120
+
121
+ // -----------------------------------------------------------------------
122
+ // _meta parsing (#1194)
123
+ // -----------------------------------------------------------------------
124
+
125
+ describe("_meta parsing", () => {
126
+ test("reads requested protocol version from _meta on initialize", async () => {
127
+ const response = await server.handleRequest({
128
+ jsonrpc: "2.0",
129
+ id: 1,
130
+ method: "initialize",
131
+ params: { _meta: { protocolVersion: "2024-11-05" } },
132
+ });
133
+ const result = response!.result as Record<string, unknown>;
134
+ expect(result.protocolVersion).toBe("2024-11-05");
135
+ });
136
+
137
+ test("_meta protocolVersion takes precedence over a legacy top-level one", async () => {
138
+ const response = await server.handleRequest({
139
+ jsonrpc: "2.0",
140
+ id: 1,
141
+ method: "initialize",
142
+ params: { protocolVersion: "2024-11-05", _meta: { protocolVersion: "2026-07-28" } },
143
+ });
144
+ const result = response!.result as Record<string, unknown>;
145
+ expect(result.protocolVersion).toBe("2026-07-28");
146
+ });
147
+
148
+ test("client identity in _meta does not interfere with tool dispatch", async () => {
149
+ const response = await server.handleRequest({
150
+ jsonrpc: "2.0",
151
+ id: 1,
152
+ method: "tools/call",
153
+ params: {
154
+ name: "search",
155
+ arguments: { query: "bucket" },
156
+ _meta: {
157
+ protocolVersion: "2026-07-28",
158
+ "io.modelcontextprotocol/clientInfo": { name: "some-agent", version: "3.0" },
159
+ },
160
+ },
161
+ });
162
+ expect(response!.error).toBeUndefined();
163
+ const result = response!.result as { content: Array<{ text: string }>; isError?: boolean };
164
+ expect(result.isError).toBeUndefined();
165
+ const parsed = JSON.parse(result.content[0].text);
166
+ expect(parsed.query).toBe("bucket");
167
+ });
168
+
169
+ test("a request with no _meta and no legacy fields is unaffected", async () => {
170
+ const response = await server.handleRequest({
171
+ jsonrpc: "2.0",
172
+ id: 1,
173
+ method: "tools/list",
174
+ params: {},
175
+ });
176
+ expect(response!.error).toBeUndefined();
177
+ });
178
+ });
179
+
180
+ // -----------------------------------------------------------------------
181
+ // Notification silence (#1194)
182
+ // -----------------------------------------------------------------------
183
+
184
+ describe("notifications", () => {
185
+ test("a message with no id gets no response", async () => {
186
+ const response = await server.handleRequest({
187
+ jsonrpc: "2.0",
188
+ method: "notifications/initialized",
189
+ params: {},
190
+ });
191
+ expect(response).toBeNull();
192
+ });
193
+
194
+ test("an id-less message is not routed through dispatch as Unknown method", async () => {
195
+ // notifications/initialized isn't a dispatch case; silence must come
196
+ // from the id check, not from swallowing a dispatch error.
197
+ const response = await server.handleRequest({
198
+ jsonrpc: "2.0",
199
+ method: "notifications/anything",
200
+ });
201
+ expect(response).toBeNull();
202
+ });
203
+
204
+ test("a message with id 0 (falsy but present) still gets a response", async () => {
205
+ const response = await server.handleRequest({
206
+ jsonrpc: "2.0",
207
+ id: 0,
208
+ method: "tools/list",
209
+ });
210
+ expect(response).not.toBeNull();
211
+ expect(response!.id).toBe(0);
212
+ });
213
+ });
214
+
215
+ // -----------------------------------------------------------------------
216
+ // server/discover (#1194)
217
+ // -----------------------------------------------------------------------
218
+
219
+ describe("server/discover", () => {
220
+ test("a 2026-07-28 client can discover capabilities without calling initialize", async () => {
221
+ const response = await server.handleRequest({
222
+ jsonrpc: "2.0",
223
+ id: 1,
224
+ method: "server/discover",
225
+ params: { _meta: { protocolVersion: "2026-07-28" } },
226
+ });
227
+ expect(response!.error).toBeUndefined();
228
+ const result = response!.result as Record<string, unknown>;
229
+ expect(result.protocolVersion).toBe("2026-07-28");
230
+ expect(result.serverInfo).toBeDefined();
231
+ expect(result.capabilities).toBeDefined();
232
+ expect(Array.isArray(result.tools)).toBe(true);
233
+ expect(Array.isArray(result.resources)).toBe(true);
234
+ });
235
+
236
+ test("discover's tools list matches tools/list", async () => {
237
+ const discover = await server.handleRequest({ jsonrpc: "2.0", id: 1, method: "server/discover", params: {} });
238
+ const list = await server.handleRequest({ jsonrpc: "2.0", id: 2, method: "tools/list" });
239
+ const discoverNames = (discover!.result as { tools: Array<{ name: string }> }).tools.map((t) => t.name).sort();
240
+ const listNames = (list!.result as { tools: Array<{ name: string }> }).tools.map((t) => t.name).sort();
241
+ expect(discoverNames).toEqual(listNames);
242
+ });
243
+
244
+ test("discover's resources list matches resources/list, including plugin contributions", async () => {
245
+ const plugin = createMockPlugin({
246
+ name: "test-lex",
247
+ mcpResources: () => [
248
+ {
249
+ uri: "catalog",
250
+ name: "Test Catalog",
251
+ description: "Test resource catalog",
252
+ mimeType: "application/json",
253
+ handler: async () => "[]",
254
+ },
255
+ ],
256
+ });
257
+ const s = new McpServer([plugin]);
258
+ const discover = await s.handleRequest({ jsonrpc: "2.0", id: 1, method: "server/discover", params: {} });
259
+ const list = await s.handleRequest({ jsonrpc: "2.0", id: 2, method: "resources/list" });
260
+ const discoverUris = (discover!.result as { resources: Array<{ uri: string }> }).resources.map((r) => r.uri).sort();
261
+ const listUris = (list!.result as { resources: Array<{ uri: string }> }).resources.map((r) => r.uri).sort();
262
+ expect(discoverUris).toEqual(listUris);
263
+ expect(discoverUris).toContain("chant://test-lex/catalog");
264
+ });
265
+
266
+ test("negotiates protocol version the same way as initialize", async () => {
267
+ const response = await server.handleRequest({
268
+ jsonrpc: "2.0",
269
+ id: 1,
270
+ method: "server/discover",
271
+ params: { protocolVersion: "2024-11-05" },
272
+ });
273
+ const result = response!.result as Record<string, unknown>;
274
+ expect(result.protocolVersion).toBe("2024-11-05");
275
+ });
276
+ });
277
+
278
+ // -----------------------------------------------------------------------
279
+ // Structured tool output (#1194)
280
+ // -----------------------------------------------------------------------
281
+
282
+ describe("structured tool output", () => {
283
+ test("an object-returning handler gets structuredContent alongside text", async () => {
284
+ const response = await server.handleRequest({
285
+ jsonrpc: "2.0",
286
+ id: 1,
287
+ method: "tools/call",
288
+ params: { name: "search", arguments: { query: "bucket" } },
289
+ });
290
+ const result = response!.result as { content: Array<{ text: string }>; structuredContent?: Record<string, unknown> };
291
+ expect(result.structuredContent).toBeDefined();
292
+ expect(result.structuredContent!.query).toBe("bucket");
293
+ const parsedText = JSON.parse(result.content[0].text);
294
+ expect(result.structuredContent).toEqual(parsedText);
295
+ });
296
+
297
+ test("a string-returning handler has no structuredContent", async () => {
298
+ const plugin = createMockPlugin({
299
+ name: "test-lex",
300
+ mcpTools: () => [
301
+ {
302
+ name: "greet",
303
+ description: "Greet",
304
+ inputSchema: { type: "object", properties: {} },
305
+ handler: async () => "hello from plugin",
306
+ },
307
+ ],
308
+ });
309
+ const s = new McpServer([plugin]);
310
+ const response = await s.handleRequest({
311
+ jsonrpc: "2.0",
312
+ id: 1,
313
+ method: "tools/call",
314
+ params: { name: "test-lex:greet", arguments: {} },
315
+ });
316
+ const result = response!.result as { structuredContent?: unknown };
317
+ expect(result.structuredContent).toBeUndefined();
318
+ });
319
+
320
+ test("an isError response has no structuredContent", async () => {
321
+ const response = await server.handleRequest({
322
+ jsonrpc: "2.0",
323
+ id: 1,
324
+ method: "tools/call",
325
+ params: { name: "unknown-tool", arguments: {} },
326
+ });
327
+ const result = response!.result as { isError: boolean; structuredContent?: unknown };
328
+ expect(result.isError).toBe(true);
329
+ expect(result.structuredContent).toBeUndefined();
330
+ });
68
331
  });
69
332
 
70
333
  describe("error handling", () => {
@@ -7,11 +7,50 @@ import { explainTool, handleExplain } from "./tools/explain";
7
7
  import { scaffoldTool, createScaffoldHandler } from "./tools/scaffold";
8
8
  import { searchTool, createSearchHandler } from "./tools/search";
9
9
  import type { LexiconPlugin } from "../../lexicon";
10
- import type { McpRequest, McpResponse, ToolDefinition, ToolHandler, ResourceDefinition } from "./types";
10
+ import type { McpRequest, McpResponse, McpRequestMeta, ToolDefinition, ToolHandler, ResourceDefinition } from "./types";
11
11
  import { createSnapshotTool, createDiffTool } from "./lifecycle-tools";
12
12
  import { createOpListTool, createOpRunTool, createOpStatusTool, createOpSignalTool, createOpReportTool } from "./op-tools";
13
13
  import { buildResourcesList, handleResourcesRead } from "./resource-handlers";
14
14
 
15
+ /**
16
+ * Protocol versions this server understands, newest first. `initialize` and
17
+ * `server/discover` both negotiate against this list rather than assuming
18
+ * the client's revision (#1194).
19
+ */
20
+ const SUPPORTED_PROTOCOL_VERSIONS = ["2026-07-28", "2024-11-05"] as const;
21
+ const LATEST_PROTOCOL_VERSION = SUPPORTED_PROTOCOL_VERSIONS[0];
22
+
23
+ /**
24
+ * Pick the protocol version to answer with: the client's requested version
25
+ * when we support it, otherwise our latest. A 2024-11-05 client that asks
26
+ * for `2024-11-05` gets it back unchanged; a 2026-07-28 client — or one
27
+ * that never says — gets the latest revision (#1194).
28
+ */
29
+ export function negotiateProtocolVersion(requested: string | undefined): string {
30
+ if (requested && (SUPPORTED_PROTOCOL_VERSIONS as readonly string[]).includes(requested)) {
31
+ return requested;
32
+ }
33
+ return LATEST_PROTOCOL_VERSION;
34
+ }
35
+
36
+ /**
37
+ * Parse the `_meta` envelope the 2026-07-28 revision puts on every request
38
+ * (client identity, requested protocol version), falling back to the
39
+ * top-level `protocolVersion`/`clientInfo` fields a prior-revision client
40
+ * sends on `initialize`. Read-side only — the server holds no handshake
41
+ * state to update (#1194).
42
+ */
43
+ export function parseMeta(params: Record<string, unknown>): { protocolVersion?: string; clientInfo?: { name: string; version?: string } } {
44
+ const meta = (params._meta ?? {}) as McpRequestMeta;
45
+ const protocolVersion =
46
+ (typeof meta.protocolVersion === "string" ? meta.protocolVersion : undefined) ??
47
+ (typeof params.protocolVersion === "string" ? params.protocolVersion : undefined);
48
+ const clientInfo =
49
+ meta["io.modelcontextprotocol/clientInfo"] ??
50
+ (params.clientInfo as { name: string; version?: string } | undefined);
51
+ return { protocolVersion, clientInfo };
52
+ }
53
+
15
54
  /**
16
55
  * The name a lexicon's MCP tool is registered under: `<lexicon>:<verb>`,
17
56
  * whether or not the lexicon already wrote the prefix itself (#1341).
@@ -135,9 +174,19 @@ export class McpServer {
135
174
  }
136
175
 
137
176
  /**
138
- * Handle incoming MCP request
177
+ * Handle incoming MCP request.
178
+ *
179
+ * A message with no `id` is a notification — `notifications/initialized`
180
+ * being the one every client sends — and gets no response in any
181
+ * protocol revision (#1194). Returns `null` for those; callers (see
182
+ * {@link start}) simply skip writing anything back.
139
183
  */
140
- async handleRequest(request: McpRequest): Promise<McpResponse> {
184
+ async handleRequest(request: McpRequest & { id: string | number }): Promise<McpResponse>;
185
+ async handleRequest(request: McpRequest): Promise<McpResponse | null>;
186
+ async handleRequest(request: McpRequest): Promise<McpResponse | null> {
187
+ if (request.id === undefined) {
188
+ return null;
189
+ }
141
190
  try {
142
191
  const result = await this.dispatch(request.method, request.params ?? {});
143
192
  return {
@@ -157,16 +206,35 @@ export class McpServer {
157
206
  }
158
207
  }
159
208
 
209
+ /**
210
+ * The `initialize` result — also the basis of `server/discover` below.
211
+ */
212
+ private buildInitializeResult(params: Record<string, unknown>): Record<string, unknown> {
213
+ const { protocolVersion } = parseMeta(params);
214
+ return {
215
+ protocolVersion: negotiateProtocolVersion(protocolVersion),
216
+ capabilities: { tools: {}, resources: {} },
217
+ serverInfo: { name: "chant", version: "0.1.0" },
218
+ };
219
+ }
220
+
160
221
  /**
161
222
  * Dispatch request to appropriate handler
162
223
  */
163
224
  private async dispatch(method: string, params: Record<string, unknown>): Promise<unknown> {
164
225
  switch (method) {
165
226
  case "initialize":
227
+ // Answered for prior-revision clients too — negotiated, not hard-coded (#1194).
228
+ return this.buildInitializeResult(params);
229
+
230
+ case "server/discover":
231
+ // On-demand capability discovery (#1194): the initialize result
232
+ // merged with the tools/resources listings, so a 2026-07-28 client
233
+ // never has to call `initialize` at all.
166
234
  return {
167
- protocolVersion: "2024-11-05",
168
- capabilities: { tools: {}, resources: {} },
169
- serverInfo: { name: "chant", version: "0.1.0" },
235
+ ...this.buildInitializeResult(params),
236
+ tools: Array.from(this.tools.values()),
237
+ resources: buildResourcesList(this.pluginResources).resources,
170
238
  };
171
239
 
172
240
  case "tools/list":
@@ -203,6 +271,7 @@ export class McpServer {
203
271
 
204
272
  try {
205
273
  const result = await handler(toolParams);
274
+ const isStructured = typeof result === "object" && result !== null;
206
275
  return {
207
276
  content: [
208
277
  {
@@ -210,6 +279,11 @@ export class McpServer {
210
279
  text: typeof result === "string" ? result : JSON.stringify(result, null, 2),
211
280
  },
212
281
  ],
282
+ // Structured output (2025-06-18+): handlers already return plain
283
+ // objects that get stringified above, so agents that want the
284
+ // parsed shape directly get it here too, alongside the text block
285
+ // kept for backward compatibility (#1194).
286
+ ...(isStructured ? { structuredContent: result } : {}),
213
287
  };
214
288
  } catch (error) {
215
289
  return {
@@ -238,7 +312,10 @@ export class McpServer {
238
312
  try {
239
313
  const request = JSON.parse(line) as McpRequest;
240
314
  const response = await this.handleRequest(request);
241
- console.log(JSON.stringify(response));
315
+ // Notifications (#1194) get no response at all — not even an empty one.
316
+ if (response !== null) {
317
+ console.log(JSON.stringify(response));
318
+ }
242
319
  } catch (error) {
243
320
  const errorResponse: McpResponse = {
244
321
  jsonrpc: "2.0",
@@ -1,9 +1,13 @@
1
1
  /**
2
2
  * MCP message types
3
+ *
4
+ * `id` is optional per JSON-RPC 2.0: a message with no `id` is a
5
+ * notification (e.g. `notifications/initialized`) and gets no response,
6
+ * in every protocol revision (#1194).
3
7
  */
4
8
  export interface McpRequest {
5
9
  jsonrpc: "2.0";
6
- id: string | number;
10
+ id?: string | number;
7
11
  method: string;
8
12
  params?: Record<string, unknown>;
9
13
  }
@@ -19,6 +23,18 @@ export interface McpResponse {
19
23
  };
20
24
  }
21
25
 
26
+ /**
27
+ * The `_meta` envelope the 2026-07-28 revision puts on every request so it
28
+ * is self-contained: client identity and the client's requested protocol
29
+ * version travel here instead of living in handshake state (#1194). A
30
+ * prior-revision client instead sends `protocolVersion`/`clientInfo` as
31
+ * top-level `initialize` params — both forms are read by {@link parseMeta}.
32
+ */
33
+ export interface McpRequestMeta {
34
+ protocolVersion?: string;
35
+ "io.modelcontextprotocol/clientInfo"?: { name: string; version?: string };
36
+ }
37
+
22
38
  /**
23
39
  * Tool definition for MCP
24
40
  */
@@ -30,6 +46,16 @@ export interface ToolDefinition {
30
46
  properties: Record<string, unknown>;
31
47
  required?: string[];
32
48
  };
49
+ /**
50
+ * JSON schema for `structuredContent` (2025-06-18+), when a tool's result
51
+ * shape is obvious and stable enough to declare. Optional and additive —
52
+ * most tools omit it (#1194).
53
+ */
54
+ outputSchema?: {
55
+ type: "object";
56
+ properties: Record<string, unknown>;
57
+ required?: string[];
58
+ };
33
59
  }
34
60
 
35
61
  /**
@@ -194,9 +194,9 @@ export interface ParsedArgs {
194
194
  check?: boolean;
195
195
  /** `chant dev surface-diff --update-snapshot --bump` — bump the lexicon's package.json version by the drift severity so the accepted surface is publishable (#616). */
196
196
  bump?: boolean;
197
- /** `chant components release record --component <name>` (#568) — component name for the release record being appended. */
197
+ /** `chant components release record --component <name>` (#568) — component name for the release record being appended. Also `chant components export <env> --component <name>` (#929) — which component's most recent recorded build to export. */
198
198
  component?: string;
199
- /** `chant components release record --digest <sha256:...>` (#568) — artifact digest to record, joining this release to the build archive/ledger. */
199
+ /** `chant components release record --digest <sha256:...>` (#568) — artifact digest to record, joining this release to the build archive/ledger. Also `chant components export --digest <manifestDigest>` (#929) — a build archive manifest digest to export directly, bypassing env/component resolution. */
200
200
  digest?: string;
201
201
  /** `chant components release record --git-sha <sha>` (#568) — git commit the deploy was built from. */
202
202
  gitSha?: string;
@@ -218,6 +218,10 @@ export interface ParsedArgs {
218
218
  fold?: boolean;
219
219
  /** `chant build --sandbox` (#1045 Phase 2) — opt-in: run-fallback source files (or every file, without `--fold`) execute together, isolated, in one sandboxed child process instead of in-process. Also settable project-wide via `chant.config.ts`'s `build.sandbox: true`; the flag always wins when set. Default (flag omitted): in-process execution, unchanged. */
220
220
  sandbox?: boolean;
221
+ /** `chant build --fold --fold-rank` (#1083) — after a `--fold` build, print blockers ranked by dominator retained-count over the forward import-failure graph, plus the separate reverse-taint bucket (chant #1044). Bare boolean form; see {@link foldRankCollapsedFile} for the file-writing form. No-op without `--fold`. */
222
+ foldRank?: boolean;
223
+ /** `chant build --fold --fold-rank <path>` (#1083) — same ranking as {@link foldRank}, ALSO exported in Brendan Gregg collapsed stack format (weighted by retained count) to `<path>`, so it renders in any flame/icicle viewer. Mutually exclusive with the bare-boolean form at the parse level (same context-sensitive lookahead as `--report`), but the text report still prints either way. */
224
+ foldRankCollapsedFile?: string;
221
225
  /** `chant build --param name=value` (#1064) — repeatable. Bound to `params.<name>` (`@intentius/chant/params`) for source to reference, after validation against `chant.config.ts`'s declared `buildParams`. Highest precedence over `--params-file`/a declared `env` mapping/the declared `default`. */
222
226
  param?: string[];
223
227
  /** `chant build --params-file <path>` (#1064) — a JSON file of `{ "name": value }` build-time parameter values. Second precedence, after `--param`. */
@@ -228,6 +232,14 @@ export interface ParsedArgs {
228
232
  * forgejo today) — the same generator `chant build --components --generate
229
233
  * <lexicon>` uses, reused rather than re-derived. */
230
234
  projection?: string;
235
+ /** `chant operator --interval <duration>` (#1485) — poll interval between rounds, e.g. "30s", "5m". Default: 60s. */
236
+ interval?: string;
237
+ /** `chant operator --lease-ttl <duration>` (#1485) — how long an acquired lease is valid before it's reclaimable by another operator. Default: 5m. */
238
+ leaseTtl?: string;
239
+ /** `chant operator --once` (#1485) — run a single round and exit, instead of looping until Ctrl-C. Also the offline test/cron-invoker story. */
240
+ once?: boolean;
241
+ /** `chant approve <op> <gate> --note <text>` (#1485) — optional free-text context recorded on the gate-resolution fact (e.g. a PR URL). */
242
+ note?: string;
231
243
  }
232
244
 
233
245
  /**
@@ -0,0 +1,42 @@
1
+ import { describe, test, expect, beforeAll, afterAll } from "vitest";
2
+ import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "fs";
3
+ import { join } from "path";
4
+ import { tmpdir } from "os";
5
+ import { scanRules } from "./docs-rule-scanning";
6
+
7
+ // #1938: a post-synth check's `description` string containing a
8
+ // backslash-escaped quote used to truncate at the first embedded `"`,
9
+ // garbling the generated docs table entry.
10
+ describe("scanRules — post-synth description extraction (#1938)", () => {
11
+ let dir: string;
12
+
13
+ beforeAll(() => {
14
+ dir = mkdtempSync(join(tmpdir(), "docs-rule-scanning-"));
15
+ mkdirSync(join(dir, "lint", "post-synth"), { recursive: true });
16
+ writeFileSync(
17
+ join(dir, "lint", "post-synth", "wk8505.ts"),
18
+ [
19
+ "export const wk8505 = {",
20
+ ' id: "WK8505",',
21
+ ' description: "A rule\'s \\"why\\" must be non-empty, and a backslash like C:\\\\path must survive intact",',
22
+ " check() { return []; },",
23
+ "};",
24
+ "",
25
+ ].join("\n"),
26
+ );
27
+ });
28
+
29
+ afterAll(() => {
30
+ rmSync(dir, { recursive: true, force: true });
31
+ });
32
+
33
+ test("extracts the full description past an escaped quote and a backslash", () => {
34
+ const rules = scanRules(dir);
35
+ const rule = rules.find((r) => r.id === "WK8505");
36
+
37
+ expect(rule).toBeDefined();
38
+ expect(rule?.description).toBe(
39
+ 'A rule\'s "why" must be non-empty, and a backslash like C:\\path must survive intact',
40
+ );
41
+ });
42
+ });
@@ -92,7 +92,7 @@ function scanDir(dir: string, type: "lint" | "post-synth", out: ScannedRule[]):
92
92
  } else {
93
93
  // Extract from PostSynthCheck objects: id, description
94
94
  const idMatch = content.match(/id:\s*"([^"]+)"/);
95
- const descMatch = content.match(/description:\s*"([^"]+)"/);
95
+ const descMatch = content.match(/description:\s*"((?:[^"\\]|\\.)*)"/);
96
96
 
97
97
  if (idMatch) {
98
98
  out.push({
@@ -100,7 +100,7 @@ function scanDir(dir: string, type: "lint" | "post-synth", out: ScannedRule[]):
100
100
  id: idMatch[1],
101
101
  severity: "error",
102
102
  category: "post-synth",
103
- description: descMatch?.[1] ?? idMatch[1],
103
+ description: descMatch ? unescapeStringLiteral(descMatch[1]) : idMatch[1],
104
104
  type: "post-synth",
105
105
  },
106
106
  source: content,
@@ -110,6 +110,29 @@ function scanDir(dir: string, type: "lint" | "post-synth", out: ScannedRule[]):
110
110
  }
111
111
  }
112
112
 
113
+ /**
114
+ * Undo the backslash escaping of a double-quoted JS/TS string literal body
115
+ * captured by a regex like `/description:\s*"((?:[^"\\]|\\.)*)"/` — the
116
+ * capture group still has the source's escape sequences (`\"`, `\\`, `\n`,
117
+ * ...) in it verbatim, so a description written as `"say \"hi\""` would
118
+ * otherwise render in generated docs as `say \"hi\"` instead of `say "hi"`.
119
+ */
120
+ function unescapeStringLiteral(raw: string): string {
121
+ return raw.replace(/\\(.)/g, (_, ch: string) => {
122
+ switch (ch) {
123
+ case "n":
124
+ return "\n";
125
+ case "t":
126
+ return "\t";
127
+ case "r":
128
+ return "\r";
129
+ default:
130
+ // \" -> ", \\ -> \, \' -> ', and anything else -> itself unescaped
131
+ return ch;
132
+ }
133
+ });
134
+ }
135
+
113
136
  function extractDescriptionFromComment(
114
137
  content: string,
115
138
  ruleId: string,
@@ -33,6 +33,13 @@ adopting chant.
33
33
  work for a component whose own name isn't the entity/resource name it
34
34
  deploys, falling back to `name` itself when omitted. See
35
35
  docs/src/content/docs/components/observability.mdx.
36
+ - **Composite mapping** — `composites` (#1492), optional: the composite kind
37
+ name(s) this component's deployed resources come from, when a consumer
38
+ joining a composite graph to the component DAG (e.g. `chant graph
39
+ --components --format ir`'s node `attrs.composites`, ../cli/handlers/
40
+ graph.ts) can't infer it by kebab-casing `name` against each composite's
41
+ kind. Omitted entirely (the default), that naming convention stays in
42
+ effect with no behavior change.
36
43
  - **The three archetypes** — service (build → publish → apply → verify),
37
44
  infra (apply → verify, no build), and producer/library (build → publish
38
45
  only, no service apply). `deploy` being a plain composition is what makes
@@ -44,8 +44,23 @@ export interface Capability<In = unknown, Out = unknown> {
44
44
  readonly kind: string;
45
45
  /** Perform the operation. */
46
46
  run(ctx: DeployContext, input: In): Promise<Out>;
47
- /** Optional paired compensation, invoked in reverse order on saga rollback. */
48
- rollback?(ctx: DeployContext, input: In): Promise<void>;
47
+ /**
48
+ * Optional paired compensation, invoked in reverse order on saga rollback.
49
+ * `output`, when supplied, is the exact value this step's own `run()` call
50
+ * returned (#1944, epic #1564 phase 4) — a serializable identity channel a
51
+ * capability can use to recover state `rollback` needs when it cannot rely
52
+ * on in-process object identity between its `run`/`rollback` calls. The
53
+ * local interpret driver (../driver.ts) always threads it through; the
54
+ * durable Temporal path (lexicons/temporal/src/component-op/*.ts) threads
55
+ * it across the Activity boundary as plain JSON, which is exactly the case
56
+ * this exists for — see ./verbs/run-agent.ts's "Rollback identity" doc
57
+ * comment for the motivating gap (a fresh sprite's checkpoint id, recorded
58
+ * only in an in-process `WeakMap` keyed by `run()`'s `input` object, never
59
+ * survives to a `rollback()` call that runs as a separate Activity with its
60
+ * own freshly-resolved `input`). Optional and additive: a capability that
61
+ * never needs it (most of them) simply ignores the third parameter.
62
+ */
63
+ rollback?(ctx: DeployContext, input: In, output?: Out): Promise<void>;
49
64
  /**
50
65
  * How this verb relates to rollback, for the COMP003 composition check.
51
66
  * Usually derivable and left unset: a capability with a `rollback` method is
@@ -371,6 +371,23 @@ export const backup =
371
371
  expect(result.liveNames?.postgres).toEqual(["pgDeployment", "pgService", "pgClaim"]);
372
372
  expect(result.liveNames?.plain).toEqual(["plain"]);
373
373
  });
374
+
375
+ test("carries each component's declared composites, absent (not defaulted) when undeclared (#1492)", async () => {
376
+ await writeFile(
377
+ join(testDir, "aws-plane.component.ts"),
378
+ `export const awsPlane = { name: "aws-plane", composites: ["ArtifactBucket", "OperatorRole"], dependsOn: [], deploy: [{ phase: "Apply", steps: [{ kind: "shell", reason: "test" }] }] };`,
379
+ );
380
+ await writeFile(
381
+ join(testDir, "plain.component.ts"),
382
+ `export const plain = { name: "plain", dependsOn: [], deploy: [{ phase: "Apply", steps: [{ kind: "shell", reason: "test" }] }] };`,
383
+ );
384
+
385
+ const result = await computeComponentGraph(testDir);
386
+
387
+ expect(result.success).toBe(true);
388
+ expect(result.composites?.["aws-plane"]).toEqual(["ArtifactBucket", "OperatorRole"]);
389
+ expect(result.composites?.plain).toBeUndefined();
390
+ });
374
391
  });
375
392
 
376
393
  // ── runComponents (#585) ─────────────────────────────────────────────────────