@collage-dam/mcp-server 0.1.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 (306) hide show
  1. package/.env.example +56 -0
  2. package/CHANGELOG.md +90 -0
  3. package/LICENSE +21 -0
  4. package/README.md +512 -0
  5. package/dist/client.d.ts +497 -0
  6. package/dist/client.d.ts.map +1 -0
  7. package/dist/client.js +1162 -0
  8. package/dist/client.js.map +1 -0
  9. package/dist/conventions/confirmation.d.ts +89 -0
  10. package/dist/conventions/confirmation.d.ts.map +1 -0
  11. package/dist/conventions/confirmation.js +132 -0
  12. package/dist/conventions/confirmation.js.map +1 -0
  13. package/dist/conventions/dry-run/batch-executor.d.ts +36 -0
  14. package/dist/conventions/dry-run/batch-executor.d.ts.map +1 -0
  15. package/dist/conventions/dry-run/batch-executor.js +89 -0
  16. package/dist/conventions/dry-run/batch-executor.js.map +1 -0
  17. package/dist/conventions/dry-run/diff-renderer.d.ts +34 -0
  18. package/dist/conventions/dry-run/diff-renderer.d.ts.map +1 -0
  19. package/dist/conventions/dry-run/diff-renderer.js +158 -0
  20. package/dist/conventions/dry-run/diff-renderer.js.map +1 -0
  21. package/dist/conventions/dry-run/index.d.ts +13 -0
  22. package/dist/conventions/dry-run/index.d.ts.map +1 -0
  23. package/dist/conventions/dry-run/index.js +10 -0
  24. package/dist/conventions/dry-run/index.js.map +1 -0
  25. package/dist/conventions/dry-run/mutating-tool.d.ts +64 -0
  26. package/dist/conventions/dry-run/mutating-tool.d.ts.map +1 -0
  27. package/dist/conventions/dry-run/mutating-tool.js +88 -0
  28. package/dist/conventions/dry-run/mutating-tool.js.map +1 -0
  29. package/dist/conventions/dry-run/summary.d.ts +66 -0
  30. package/dist/conventions/dry-run/summary.d.ts.map +1 -0
  31. package/dist/conventions/dry-run/summary.js +185 -0
  32. package/dist/conventions/dry-run/summary.js.map +1 -0
  33. package/dist/conventions/dry-run/types.d.ts +597 -0
  34. package/dist/conventions/dry-run/types.d.ts.map +1 -0
  35. package/dist/conventions/dry-run/types.js +108 -0
  36. package/dist/conventions/dry-run/types.js.map +1 -0
  37. package/dist/conventions/dry-run/with-dry-run.d.ts +66 -0
  38. package/dist/conventions/dry-run/with-dry-run.d.ts.map +1 -0
  39. package/dist/conventions/dry-run/with-dry-run.js +219 -0
  40. package/dist/conventions/dry-run/with-dry-run.js.map +1 -0
  41. package/dist/conventions/env.d.ts +49 -0
  42. package/dist/conventions/env.d.ts.map +1 -0
  43. package/dist/conventions/env.js +84 -0
  44. package/dist/conventions/env.js.map +1 -0
  45. package/dist/conventions/errors.d.ts +68 -0
  46. package/dist/conventions/errors.d.ts.map +1 -0
  47. package/dist/conventions/errors.js +81 -0
  48. package/dist/conventions/errors.js.map +1 -0
  49. package/dist/conventions/logger.d.ts +28 -0
  50. package/dist/conventions/logger.d.ts.map +1 -0
  51. package/dist/conventions/logger.js +105 -0
  52. package/dist/conventions/logger.js.map +1 -0
  53. package/dist/conventions/pagination.d.ts +37 -0
  54. package/dist/conventions/pagination.d.ts.map +1 -0
  55. package/dist/conventions/pagination.js +53 -0
  56. package/dist/conventions/pagination.js.map +1 -0
  57. package/dist/conventions/rate-limiter.d.ts +54 -0
  58. package/dist/conventions/rate-limiter.d.ts.map +1 -0
  59. package/dist/conventions/rate-limiter.js +143 -0
  60. package/dist/conventions/rate-limiter.js.map +1 -0
  61. package/dist/conventions/response-budget.d.ts +66 -0
  62. package/dist/conventions/response-budget.d.ts.map +1 -0
  63. package/dist/conventions/response-budget.js +89 -0
  64. package/dist/conventions/response-budget.js.map +1 -0
  65. package/dist/conventions/schema-version.d.ts +27 -0
  66. package/dist/conventions/schema-version.d.ts.map +1 -0
  67. package/dist/conventions/schema-version.js +29 -0
  68. package/dist/conventions/schema-version.js.map +1 -0
  69. package/dist/conventions/state-store-redis.d.ts +32 -0
  70. package/dist/conventions/state-store-redis.d.ts.map +1 -0
  71. package/dist/conventions/state-store-redis.js +77 -0
  72. package/dist/conventions/state-store-redis.js.map +1 -0
  73. package/dist/conventions/state-store.d.ts +46 -0
  74. package/dist/conventions/state-store.d.ts.map +1 -0
  75. package/dist/conventions/state-store.js +105 -0
  76. package/dist/conventions/state-store.js.map +1 -0
  77. package/dist/index.d.ts +5 -0
  78. package/dist/index.d.ts.map +1 -0
  79. package/dist/index.js +421 -0
  80. package/dist/index.js.map +1 -0
  81. package/dist/prompts/collection-audit.d.ts +13 -0
  82. package/dist/prompts/collection-audit.d.ts.map +1 -0
  83. package/dist/prompts/collection-audit.js +168 -0
  84. package/dist/prompts/collection-audit.js.map +1 -0
  85. package/dist/prompts/create-distribution.d.ts +15 -0
  86. package/dist/prompts/create-distribution.d.ts.map +1 -0
  87. package/dist/prompts/create-distribution.js +111 -0
  88. package/dist/prompts/create-distribution.js.map +1 -0
  89. package/dist/prompts/helpers.d.ts +20 -0
  90. package/dist/prompts/helpers.d.ts.map +1 -0
  91. package/dist/prompts/helpers.js +53 -0
  92. package/dist/prompts/helpers.js.map +1 -0
  93. package/dist/prompts/library-health-audit.d.ts +13 -0
  94. package/dist/prompts/library-health-audit.d.ts.map +1 -0
  95. package/dist/prompts/library-health-audit.js +131 -0
  96. package/dist/prompts/library-health-audit.js.map +1 -0
  97. package/dist/prompts/usage-insights.d.ts +13 -0
  98. package/dist/prompts/usage-insights.d.ts.map +1 -0
  99. package/dist/prompts/usage-insights.js +98 -0
  100. package/dist/prompts/usage-insights.js.map +1 -0
  101. package/dist/prompts/wrap-prompt-as-tool.d.ts +48 -0
  102. package/dist/prompts/wrap-prompt-as-tool.d.ts.map +1 -0
  103. package/dist/prompts/wrap-prompt-as-tool.js +61 -0
  104. package/dist/prompts/wrap-prompt-as-tool.js.map +1 -0
  105. package/dist/resources/asset-by-id.d.ts +4 -0
  106. package/dist/resources/asset-by-id.d.ts.map +1 -0
  107. package/dist/resources/asset-by-id.js +27 -0
  108. package/dist/resources/asset-by-id.js.map +1 -0
  109. package/dist/resources/collections.d.ts +5 -0
  110. package/dist/resources/collections.d.ts.map +1 -0
  111. package/dist/resources/collections.js +48 -0
  112. package/dist/resources/collections.js.map +1 -0
  113. package/dist/resources/custom-fields.d.ts +4 -0
  114. package/dist/resources/custom-fields.d.ts.map +1 -0
  115. package/dist/resources/custom-fields.js +30 -0
  116. package/dist/resources/custom-fields.js.map +1 -0
  117. package/dist/resources/folders.d.ts +5 -0
  118. package/dist/resources/folders.d.ts.map +1 -0
  119. package/dist/resources/folders.js +73 -0
  120. package/dist/resources/folders.js.map +1 -0
  121. package/dist/resources/helpers.d.ts +17 -0
  122. package/dist/resources/helpers.d.ts.map +1 -0
  123. package/dist/resources/helpers.js +59 -0
  124. package/dist/resources/helpers.js.map +1 -0
  125. package/dist/resources/portals.d.ts +5 -0
  126. package/dist/resources/portals.d.ts.map +1 -0
  127. package/dist/resources/portals.js +81 -0
  128. package/dist/resources/portals.js.map +1 -0
  129. package/dist/resources/recent-and-dashboard.d.ts +5 -0
  130. package/dist/resources/recent-and-dashboard.d.ts.map +1 -0
  131. package/dist/resources/recent-and-dashboard.js +42 -0
  132. package/dist/resources/recent-and-dashboard.js.map +1 -0
  133. package/dist/tools/asset-selection.d.ts +102 -0
  134. package/dist/tools/asset-selection.d.ts.map +1 -0
  135. package/dist/tools/asset-selection.js +133 -0
  136. package/dist/tools/asset-selection.js.map +1 -0
  137. package/dist/tools/audit/audit-folder-structure.d.ts +108 -0
  138. package/dist/tools/audit/audit-folder-structure.d.ts.map +1 -0
  139. package/dist/tools/audit/audit-folder-structure.js +260 -0
  140. package/dist/tools/audit/audit-folder-structure.js.map +1 -0
  141. package/dist/tools/audit/audit-naming-conventions.d.ts +83 -0
  142. package/dist/tools/audit/audit-naming-conventions.d.ts.map +1 -0
  143. package/dist/tools/audit/audit-naming-conventions.js +238 -0
  144. package/dist/tools/audit/audit-naming-conventions.js.map +1 -0
  145. package/dist/tools/audit/audit-tagging-hygiene.d.ts +77 -0
  146. package/dist/tools/audit/audit-tagging-hygiene.d.ts.map +1 -0
  147. package/dist/tools/audit/audit-tagging-hygiene.js +402 -0
  148. package/dist/tools/audit/audit-tagging-hygiene.js.map +1 -0
  149. package/dist/tools/audit/detect-duplicates.d.ts +62 -0
  150. package/dist/tools/audit/detect-duplicates.d.ts.map +1 -0
  151. package/dist/tools/audit/detect-duplicates.js +0 -0
  152. package/dist/tools/audit/detect-duplicates.js.map +1 -0
  153. package/dist/tools/audit/types.d.ts +526 -0
  154. package/dist/tools/audit/types.d.ts.map +1 -0
  155. package/dist/tools/audit/types.js +188 -0
  156. package/dist/tools/audit/types.js.map +1 -0
  157. package/dist/tools/bulk-move-assets.d.ts +78 -0
  158. package/dist/tools/bulk-move-assets.d.ts.map +1 -0
  159. package/dist/tools/bulk-move-assets.js +122 -0
  160. package/dist/tools/bulk-move-assets.js.map +1 -0
  161. package/dist/tools/bulk-normalize-filenames.d.ts +62 -0
  162. package/dist/tools/bulk-normalize-filenames.d.ts.map +1 -0
  163. package/dist/tools/bulk-normalize-filenames.js +237 -0
  164. package/dist/tools/bulk-normalize-filenames.js.map +1 -0
  165. package/dist/tools/bulk-rename-assets.d.ts +79 -0
  166. package/dist/tools/bulk-rename-assets.d.ts.map +1 -0
  167. package/dist/tools/bulk-rename-assets.js +139 -0
  168. package/dist/tools/bulk-rename-assets.js.map +1 -0
  169. package/dist/tools/bulk-tags.d.ts +107 -0
  170. package/dist/tools/bulk-tags.d.ts.map +1 -0
  171. package/dist/tools/bulk-tags.js +220 -0
  172. package/dist/tools/bulk-tags.js.map +1 -0
  173. package/dist/tools/client-adapters.d.ts +76 -0
  174. package/dist/tools/client-adapters.d.ts.map +1 -0
  175. package/dist/tools/client-adapters.js +648 -0
  176. package/dist/tools/client-adapters.js.map +1 -0
  177. package/dist/tools/collection-membership.d.ts +90 -0
  178. package/dist/tools/collection-membership.d.ts.map +1 -0
  179. package/dist/tools/collection-membership.js +195 -0
  180. package/dist/tools/collection-membership.js.map +1 -0
  181. package/dist/tools/create-collection.d.ts +63 -0
  182. package/dist/tools/create-collection.d.ts.map +1 -0
  183. package/dist/tools/create-collection.js +151 -0
  184. package/dist/tools/create-collection.js.map +1 -0
  185. package/dist/tools/create-folder.d.ts +46 -0
  186. package/dist/tools/create-folder.d.ts.map +1 -0
  187. package/dist/tools/create-folder.js +83 -0
  188. package/dist/tools/create-folder.js.map +1 -0
  189. package/dist/tools/create-share-link.d.ts +107 -0
  190. package/dist/tools/create-share-link.d.ts.map +1 -0
  191. package/dist/tools/create-share-link.js +239 -0
  192. package/dist/tools/create-share-link.js.map +1 -0
  193. package/dist/tools/get-asset-details.d.ts +401 -0
  194. package/dist/tools/get-asset-details.d.ts.map +1 -0
  195. package/dist/tools/get-asset-details.js +56 -0
  196. package/dist/tools/get-asset-details.js.map +1 -0
  197. package/dist/tools/get-collection.d.ts +126 -0
  198. package/dist/tools/get-collection.d.ts.map +1 -0
  199. package/dist/tools/get-collection.js +52 -0
  200. package/dist/tools/get-collection.js.map +1 -0
  201. package/dist/tools/get-embed-code.d.ts +195 -0
  202. package/dist/tools/get-embed-code.d.ts.map +1 -0
  203. package/dist/tools/get-embed-code.js +214 -0
  204. package/dist/tools/get-embed-code.js.map +1 -0
  205. package/dist/tools/insights/analyze-share-links.d.ts +159 -0
  206. package/dist/tools/insights/analyze-share-links.d.ts.map +1 -0
  207. package/dist/tools/insights/analyze-share-links.js +314 -0
  208. package/dist/tools/insights/analyze-share-links.js.map +1 -0
  209. package/dist/tools/insights/insight-cache.d.ts +36 -0
  210. package/dist/tools/insights/insight-cache.d.ts.map +1 -0
  211. package/dist/tools/insights/insight-cache.js +98 -0
  212. package/dist/tools/insights/insight-cache.js.map +1 -0
  213. package/dist/tools/insights/report-asset-activation.d.ts +149 -0
  214. package/dist/tools/insights/report-asset-activation.d.ts.map +1 -0
  215. package/dist/tools/insights/report-asset-activation.js +380 -0
  216. package/dist/tools/insights/report-asset-activation.js.map +1 -0
  217. package/dist/tools/insights/report-stale-assets.d.ts +120 -0
  218. package/dist/tools/insights/report-stale-assets.d.ts.map +1 -0
  219. package/dist/tools/insights/report-stale-assets.js +281 -0
  220. package/dist/tools/insights/report-stale-assets.js.map +1 -0
  221. package/dist/tools/insights/report-top-assets.d.ts +139 -0
  222. package/dist/tools/insights/report-top-assets.d.ts.map +1 -0
  223. package/dist/tools/insights/report-top-assets.js +407 -0
  224. package/dist/tools/insights/report-top-assets.js.map +1 -0
  225. package/dist/tools/list-categories.d.ts +127 -0
  226. package/dist/tools/list-categories.d.ts.map +1 -0
  227. package/dist/tools/list-categories.js +68 -0
  228. package/dist/tools/list-categories.js.map +1 -0
  229. package/dist/tools/list-collections.d.ts +127 -0
  230. package/dist/tools/list-collections.d.ts.map +1 -0
  231. package/dist/tools/list-collections.js +53 -0
  232. package/dist/tools/list-collections.js.map +1 -0
  233. package/dist/tools/list-custom-fields.d.ts +125 -0
  234. package/dist/tools/list-custom-fields.d.ts.map +1 -0
  235. package/dist/tools/list-custom-fields.js +51 -0
  236. package/dist/tools/list-custom-fields.js.map +1 -0
  237. package/dist/tools/list-share-links.d.ts +192 -0
  238. package/dist/tools/list-share-links.d.ts.map +1 -0
  239. package/dist/tools/list-share-links.js +92 -0
  240. package/dist/tools/list-share-links.js.map +1 -0
  241. package/dist/tools/list-workspaces.d.ts +88 -0
  242. package/dist/tools/list-workspaces.d.ts.map +1 -0
  243. package/dist/tools/list-workspaces.js +71 -0
  244. package/dist/tools/list-workspaces.js.map +1 -0
  245. package/dist/tools/move-asset.d.ts +48 -0
  246. package/dist/tools/move-asset.d.ts.map +1 -0
  247. package/dist/tools/move-asset.js +85 -0
  248. package/dist/tools/move-asset.js.map +1 -0
  249. package/dist/tools/rename-asset.d.ts +88 -0
  250. package/dist/tools/rename-asset.d.ts.map +1 -0
  251. package/dist/tools/rename-asset.js +100 -0
  252. package/dist/tools/rename-asset.js.map +1 -0
  253. package/dist/tools/rename-folder.d.ts +55 -0
  254. package/dist/tools/rename-folder.d.ts.map +1 -0
  255. package/dist/tools/rename-folder.js +101 -0
  256. package/dist/tools/rename-folder.js.map +1 -0
  257. package/dist/tools/revoke-share-link.d.ts +55 -0
  258. package/dist/tools/revoke-share-link.d.ts.map +1 -0
  259. package/dist/tools/revoke-share-link.js +77 -0
  260. package/dist/tools/revoke-share-link.js.map +1 -0
  261. package/dist/tools/search/facets.d.ts +34 -0
  262. package/dist/tools/search/facets.d.ts.map +1 -0
  263. package/dist/tools/search/facets.js +147 -0
  264. package/dist/tools/search/facets.js.map +1 -0
  265. package/dist/tools/search/filter-builder.d.ts +33 -0
  266. package/dist/tools/search/filter-builder.d.ts.map +1 -0
  267. package/dist/tools/search/filter-builder.js +111 -0
  268. package/dist/tools/search/filter-builder.js.map +1 -0
  269. package/dist/tools/search/search-assets.d.ts +41 -0
  270. package/dist/tools/search/search-assets.d.ts.map +1 -0
  271. package/dist/tools/search/search-assets.js +162 -0
  272. package/dist/tools/search/search-assets.js.map +1 -0
  273. package/dist/tools/search/search-collections.d.ts +35 -0
  274. package/dist/tools/search/search-collections.d.ts.map +1 -0
  275. package/dist/tools/search/search-collections.js +103 -0
  276. package/dist/tools/search/search-collections.js.map +1 -0
  277. package/dist/tools/search/types.d.ts +1047 -0
  278. package/dist/tools/search/types.d.ts.map +1 -0
  279. package/dist/tools/search/types.js +216 -0
  280. package/dist/tools/search/types.js.map +1 -0
  281. package/dist/tools/update-asset-metadata.d.ts +78 -0
  282. package/dist/tools/update-asset-metadata.d.ts.map +1 -0
  283. package/dist/tools/update-asset-metadata.js +203 -0
  284. package/dist/tools/update-asset-metadata.js.map +1 -0
  285. package/dist/tools/update-collection.d.ts +69 -0
  286. package/dist/tools/update-collection.d.ts.map +1 -0
  287. package/dist/tools/update-collection.js +142 -0
  288. package/dist/tools/update-collection.js.map +1 -0
  289. package/dist/tools/view-category-contents.d.ts +231 -0
  290. package/dist/tools/view-category-contents.d.ts.map +1 -0
  291. package/dist/tools/view-category-contents.js +97 -0
  292. package/dist/tools/view-category-contents.js.map +1 -0
  293. package/dist/types.d.ts +1326 -0
  294. package/dist/types.d.ts.map +1 -0
  295. package/dist/types.js +288 -0
  296. package/dist/types.js.map +1 -0
  297. package/dist/typesense.d.ts +84 -0
  298. package/dist/typesense.d.ts.map +1 -0
  299. package/dist/typesense.js +243 -0
  300. package/dist/typesense.js.map +1 -0
  301. package/docs/api-field-verification.md +244 -0
  302. package/docs/deployment-runbook.md +446 -0
  303. package/docs/security-review.md +195 -0
  304. package/docs/typesense-filter-schema.md +262 -0
  305. package/docs/verified-endpoints.md +38 -0
  306. package/package.json +72 -0
@@ -0,0 +1,446 @@
1
+ # Collage MCP Server — Deployment Runbook
2
+
3
+ Operational reference for Collage engineering. Covers environment
4
+ configuration, transport options, state store provisioning, observability,
5
+ secret rotation, health checks, and incident-response patterns.
6
+
7
+ > Companion docs:
8
+ > - [`README.md`](../README.md) — quickstart and surface overview
9
+ > - [`docs/api-field-verification.md`](./api-field-verification.md) — API quirks reference (handoff deliverable)
10
+ > - [`docs/verified-endpoints.md`](./verified-endpoints.md) — Phase-2.5 endpoint stub list (superseded by api-field-verification)
11
+ > - [`docs/typesense-filter-schema.md`](./typesense-filter-schema.md) — search filter shape
12
+
13
+ ---
14
+
15
+ ## Environment configuration
16
+
17
+ ### Required env vars
18
+
19
+ | Var | Purpose | Failure mode if missing |
20
+ | --- | --- | --- |
21
+ | `COLLAGE_API_KEY` | Bearer token for the Collage REST API. Also forwarded to the Nuxt search proxy as the auth credential. | Server refuses to start with a `Missing required environment variable` error. |
22
+ | `COLLAGE_WORKSPACE_ID` | Numeric workspace ID (the value in app.collage.inc URLs). | Server refuses to start. |
23
+ | `MCP_CONFIRMATION_SECRET` | HMAC secret for dry-run confirmation tokens. Must be high-entropy (32+ bytes). | Server refuses to start. |
24
+
25
+ ### Optional env vars
26
+
27
+ | Var | Default | Purpose |
28
+ | --- | --- | --- |
29
+ | `COLLAGE_API_URL` | `https://damapi.collage.inc/api/v1/` | Base URL for the Laravel REST API. |
30
+ | `COLLAGE_SEARCH_BASE` | `https://app.collage.inc` | Base URL for the Nuxt `/typesense/search` proxy. |
31
+ | `COLLAGE_SEARCH_TIMEOUT_MS` | `15000` | Search request timeout. Per-request `AbortController`. |
32
+ | `COLLAGE_MAX_RPS` | `10` | Token-bucket rate limit on the REST client. 429s are retried with backoff. |
33
+ | `REDIS_URL` | _unset_ → in-memory state store | Connection string for the dry-run plan store. Required for streamable-HTTP transport in production. |
34
+ | `LOG_LEVEL` | `info` | `debug` / `info` / `warn` / `error`. Logs go to stderr. |
35
+
36
+ ### What `COLLAGE_API_KEY` actually is
37
+
38
+ The Collage REST API doesn't issue dedicated long-lived API keys today.
39
+ The "key" is the JWT issued by `POST /api/v1/login` and stored in the
40
+ Nuxt frontend after a normal user sign-in. In practice you extract it
41
+ from a browser session.
42
+
43
+ **Implications for production deployments:**
44
+
45
+ - The token is bound to the signed-in user's session. It expires when
46
+ the user's session does (typical Nuxt-auth defaults).
47
+ - For long-lived servers, use a **dedicated service-account user**
48
+ whose session is controlled by ops, not a real engineer.
49
+ - Rotate the token on a schedule that matches the upstream session
50
+ lifetime — see "Secret rotation" below.
51
+
52
+ This is a known limitation. Collage's roadmap includes a proper API-key
53
+ system; track that separately. Until then, the JWT-extracted-from-UI
54
+ flow is the only option.
55
+
56
+ ---
57
+
58
+ ## Transport options
59
+
60
+ The server supports two MCP transports. Pick based on deployment shape.
61
+
62
+ ### stdio (recommended for single-user clients)
63
+
64
+ The default. Each MCP client (Claude Desktop, Cursor, etc.) spawns a
65
+ dedicated `node dist/index.js` subprocess and talks over stdin/stdout.
66
+ State is per-process and in-memory.
67
+
68
+ **Pros:**
69
+ - Zero infrastructure beyond Node.
70
+ - Per-user isolation — every client gets a fresh process.
71
+ - Logs go to stderr; the parent client surfaces them.
72
+
73
+ **Cons:**
74
+ - One process per client. Doesn't scale to a centralized multi-user deploy.
75
+ - Restart on every client launch — cold-start cost is small but real.
76
+
77
+ **When to use:** developer machines, embedded into desktop AI clients, demo deployments.
78
+
79
+ ### streamable-HTTP (recommended for centralized / multi-user deploys)
80
+
81
+ Run the server as a long-lived HTTP service. Multiple MCP clients
82
+ connect over HTTP+SSE. Dry-run plans live in Redis so they survive
83
+ across requests and across processes.
84
+
85
+ **Pros:**
86
+ - One server, many clients.
87
+ - Plans persist across client reconnects.
88
+ - Standard observability (request logging, metrics, health checks).
89
+
90
+ **Cons:**
91
+ - Requires Redis.
92
+ - Additional ops surface — TLS termination, ingress, auth at the
93
+ proxy layer.
94
+
95
+ **Status:** wired but not enabled in the default entrypoint. Tracked
96
+ in `mcp-server-scaffold-auth` T15. When ready to enable: register
97
+ `StreamableHTTPServerTransport` (instead of `StdioServerTransport`)
98
+ in `src/index.ts`, set `REDIS_URL`, and front the server with your
99
+ HTTPS terminator.
100
+
101
+ ---
102
+
103
+ ## State store provisioning
104
+
105
+ ### In-memory (default for stdio)
106
+
107
+ Used automatically when `REDIS_URL` is unset. Plans are stored in
108
+ process memory and disappear on restart. Adequate for stdio because
109
+ each client has its own process.
110
+
111
+ ### Redis (required for streamable-HTTP, optional for stdio)
112
+
113
+ For streamable-HTTP deployments, plans must survive across reconnects.
114
+ Set `REDIS_URL` to a connection string Pino's `ioredis` understands:
115
+
116
+ ```
117
+ redis://[username:password@]host:port[/db]
118
+ rediss://... # TLS
119
+ ```
120
+
121
+ For local development, `docker compose up redis` provides a
122
+ `redis:7-alpine` instance on `redis://localhost:6379` (config in
123
+ [`docker-compose.yml`](../docker-compose.yml)).
124
+
125
+ For production, run Redis in a managed service (AWS ElastiCache, GCP
126
+ Memorystore, Upstash, Redis Cloud). Sizing: plans are small (~1KB
127
+ typical, max ~10KB for large bulk operations). Memory cost is
128
+ negligible. The store TTLs entries at `confirmation.memo_ttl_seconds`
129
+ (default 60 min); no explicit eviction policy required.
130
+
131
+ ---
132
+
133
+ ## Health checks
134
+
135
+ The server doesn't expose an HTTP health endpoint by default (stdio
136
+ has no HTTP surface). For streamable-HTTP deployments, the standard
137
+ checks:
138
+
139
+ - **TCP**: server is listening on the configured port.
140
+ - **HTTP `GET /health`** (when registered): returns 200 with
141
+ `{ status: "ok", uptime_ms, version }`. Not yet wired — add when
142
+ enabling streamable-HTTP.
143
+ - **MCP `initialize` round-trip**: the canonical liveness check. A
144
+ smoke client (see `scripts/smoke-stdio.ts`) connects, sends
145
+ `initialize`, expects `serverCapabilities`. Run on a 60-second
146
+ interval against staging.
147
+
148
+ For stdio deployments embedded in desktop clients, "health" is
149
+ end-to-end: the client surfaces server errors directly. Monitor at
150
+ the client layer.
151
+
152
+ ---
153
+
154
+ ## Logging conventions
155
+
156
+ The server uses [pino](https://getpino.io) for structured JSON logs
157
+ (stderr only — stdout is reserved for MCP framing). Every log line
158
+ includes:
159
+
160
+ - `level` (`trace` / `debug` / `info` / `warn` / `error` / `fatal`)
161
+ - `ts` (ISO 8601 timestamp)
162
+ - `pid`, `hostname`
163
+ - `request_id` (ULID, propagated through every CollageClient call)
164
+ - `msg` (human-readable summary)
165
+
166
+ ### What's redacted
167
+
168
+ The pino logger is configured with a redaction list — secret fields
169
+ never appear in logs. See [`src/conventions/logger.ts`](../src/conventions/logger.ts).
170
+ Currently redacted:
171
+
172
+ - `Authorization` headers
173
+ - `COLLAGE_API_KEY` value if accidentally logged
174
+ - `confirmation_token` payloads (signed; not strictly secret, but
175
+ noisy)
176
+
177
+ ### How to find a request
178
+
179
+ Every tool invocation generates a `request_id`. The MCP response
180
+ includes it in the structured content; the LLM surfaces it on
181
+ errors. Search logs by `request_id` to find the full upstream
182
+ chain — REST calls, search proxy POSTs, dry-run plan/execute, etc.
183
+
184
+ ```bash
185
+ node dist/index.js 2>&1 | jq 'select(.request_id == "01KQT...")'
186
+ ```
187
+
188
+ ---
189
+
190
+ ## Secret rotation
191
+
192
+ ### Rotating `COLLAGE_API_KEY`
193
+
194
+ Until Collage ships a real API-key system, this is a manual flow:
195
+
196
+ 1. Sign into the Collage UI as the service-account user.
197
+ 2. Trigger any authenticated request; copy the new `Authorization`
198
+ header value.
199
+ 3. Update the environment variable wherever it's stored
200
+ (`.env`, secret manager, container env, Claude Desktop config).
201
+ 4. Restart the MCP server process.
202
+
203
+ In-flight tool calls fail with `UNAUTHORIZED`; the LLM re-tries
204
+ naturally. There's no token-refresh flow today; we may add one if
205
+ Collage exposes a refresh-token endpoint.
206
+
207
+ ### Rotating `MCP_CONFIRMATION_SECRET`
208
+
209
+ By design, rotating this secret invalidates **every in-flight
210
+ confirmation token**. That's the intended way to revoke stale
211
+ plans (e.g. after a security incident):
212
+
213
+ 1. Generate a new high-entropy value: `openssl rand -hex 32`.
214
+ 2. Update the environment variable.
215
+ 3. Restart the server process.
216
+
217
+ After restart, every preview-then-confirm flow that started under
218
+ the old secret fails the HMAC check on execute and is re-prompted
219
+ from scratch. The dry-run framework treats this as a hard divergence,
220
+ not a recoverable error — that's the point.
221
+
222
+ ---
223
+
224
+ ## Degraded-mode behaviors
225
+
226
+ The server is designed to keep working even when individual upstream
227
+ surfaces are unavailable. Here's what degrades and how, so ops can
228
+ recognize each pattern:
229
+
230
+ ### Search proxy unavailable
231
+
232
+ If `app.collage.inc/typesense/search` returns 502/503 or times out:
233
+
234
+ - `search_assets` and `search_collections` return a typed `UPSTREAM`
235
+ error with the request_id.
236
+ - The `library_health_audit` prompt's playbook explicitly handles
237
+ this: it walks `view_category_contents` per folder as a fallback
238
+ for small workspaces (~< 500 assets) or surfaces an `UPSTREAM`
239
+ finding for larger ones (where the fallback is too expensive).
240
+ - Other tools / resources are unaffected — the REST-side surface
241
+ uses a separate host (`damapi.collage.inc`).
242
+
243
+ ### Search architecture — interim state and migration path
244
+
245
+ v0.1.0 ships with `COLLAGE_SEARCH_BASE` pointing at the Nuxt
246
+ `/typesense/search` middleware on `app.collage.inc`. Ross Durbin
247
+ confirmed (2026-05-05) that Collage will build a first-party REST
248
+ endpoint at `damapi.collage.inc` that supersedes this hop:
249
+
250
+ - **Endpoint:** `POST /api/v1/digital-assets/search` (path TBD until
251
+ shipped). Body shape: `{ collections: ["digital_assets",
252
+ "dam_collections"], ... }`, max 3 collections per request, with
253
+ per-collection `query_by` / `facet_by` / `sort_by` overrides.
254
+ - **Build window:** Week 1 (route + multiSearch dispatch + compact
255
+ projection) → Week 2 (filter translator + verbose projection +
256
+ facet defaults + error mapping) → buffer for QA + a staging
257
+ endpoint we can hit. Subject to Collage sprint priority.
258
+ - **Cutover for Collage ops:** when the new endpoint is live and
259
+ staging-verified, change `COLLAGE_SEARCH_BASE` from
260
+ `https://app.collage.inc` to the new host. A coordinated MCP-side
261
+ release will land the path + body-shape changes; the env-var swap
262
+ is the only ops-side action required at cutover.
263
+ - **Rate-limit posture post-cutover:** the new endpoint will share
264
+ the default `/api/v1/*` bucket (600 req/min per user) and likely
265
+ add a dedicated search bucket of 120 req/min per user. Tune
266
+ `COLLAGE_MAX_RPS` to **2** (= 120 req/min) on the worker handling
267
+ audit walks so the burst worst case (~400 calls on a 40-folder
268
+ workspace) doesn't trip the dedicated bucket. The default of 10
269
+ was sized for the proxy era and will be over-budget post-cutover.
270
+ - **429 handling:** the current client already respects `Retry-After`
271
+ with exponential backoff; no changes required.
272
+
273
+ ### REST API rate-limited
274
+
275
+ The token-bucket limiter (`COLLAGE_MAX_RPS`, default 10) caps
276
+ outbound requests. If upstream returns 429, the client backs off
277
+ and retries up to 3 times with exponential delay. Persistent 429s
278
+ return `RATE_LIMITED` to the caller.
279
+
280
+ Tune `COLLAGE_MAX_RPS` down if you observe sustained 429s in logs.
281
+ The default is conservative for typical workspace traffic.
282
+
283
+ ### Empty facets on `search_assets`
284
+
285
+ The Nuxt search proxy strips `facet_by` before forwarding to
286
+ Typesense (proxy-side limitation, not a server bug). Effect:
287
+ `facets.tags`, `facets.file_type`, `facets.date_histogram` always
288
+ return `[]`. `next_step_hints` degrades gracefully via per-hit
289
+ `countUntagged()`. This is a steady-state behavior, not a transient
290
+ failure — tracked in the issue dashboard until upstream extends the
291
+ proxy.
292
+
293
+ ### Dry-run confirmation expired
294
+
295
+ If a plan's soft expiry (15 min) elapses but the memo store still
296
+ has the plan record, the framework auto-refreshes:
297
+
298
+ 1. Re-runs `plan(input)` with the same caller input.
299
+ 2. Computes a fingerprint over the new change list (sorted by id;
300
+ tool-internal metadata excluded).
301
+ 3. **Match** → emits a `dry_run.auto_refresh` log entry, executes on
302
+ the prior plan. No caller action needed.
303
+ 4. **Mismatch** → returns `CONFIRMATION_STATE_DIVERGED` with both
304
+ plan summaries in `error.cause.prior_plan` and
305
+ `error.cause.new_plan`. The LLM should re-preview and re-confirm.
306
+
307
+ If the memo expiry (60 min) elapses, the plan is genuinely gone —
308
+ the LLM gets `NOT_FOUND` and must start over.
309
+
310
+ ### Folder tree size at scale
311
+
312
+ `listAllCategoriesRecursive()` is bounded by `maxNodes` (default 5000)
313
+ and `maxDepth` (default 16). On a workspace with > 5000 folders, the
314
+ walk truncates and consumers see a partial tree. Override the bounds
315
+ via constructor arguments if your deployment needs deeper coverage —
316
+ budget against the 12K-token response cap accordingly.
317
+
318
+ ---
319
+
320
+ ## Observability checklist
321
+
322
+ For production streamable-HTTP deployments, monitor:
323
+
324
+ - [ ] Process uptime (alert on restart loops)
325
+ - [ ] Response latency p50/p95/p99 per tool
326
+ - [ ] Error rate per tool (`isError: true` rate)
327
+ - [ ] `dry_run.auto_refresh` log frequency (high frequency suggests
328
+ callers are pausing too long; consider raising soft TTL)
329
+ - [ ] `dry_run.divergence` log frequency (spikes suggest concurrent
330
+ mutations; investigate)
331
+ - [ ] Upstream 429 rate (tune `COLLAGE_MAX_RPS`)
332
+ - [ ] Upstream 5xx rate (track separately for `damapi.collage.inc`
333
+ and `app.collage.inc`)
334
+ - [ ] Redis connection health (if streamable-HTTP)
335
+ - [ ] Memory usage (in-memory state store grows under stdio load)
336
+
337
+ Pino structured logs feed cleanly into Datadog, CloudWatch, Loki,
338
+ etc. via stderr capture.
339
+
340
+ ---
341
+
342
+ ## Incident playbook
343
+
344
+ ### "All tools return UNAUTHORIZED"
345
+
346
+ Most likely: `COLLAGE_API_KEY` JWT expired (the upstream session
347
+ ended). Re-extract the token from the service-account user's
348
+ browser session, update env, restart. See "Rotating
349
+ `COLLAGE_API_KEY`" above.
350
+
351
+ ### "search_assets returns UPSTREAM, other tools fine"
352
+
353
+ Most likely: the Nuxt `/typesense/search` proxy is down, or
354
+ `app.collage.inc` is in a bad deploy state. Confirm:
355
+
356
+ ```bash
357
+ curl -sS -X POST -H "Authorization: Bearer $COLLAGE_API_KEY" \
358
+ -H "Content-Type: application/json" \
359
+ -d '{"request":{"q":"*","collections":["digital_assets"]},
360
+ "commonSearchParams":{"query_by":"search_name","per_page":1,"page":1},
361
+ "workspace_id":'"$COLLAGE_WORKSPACE_ID"'}' \
362
+ https://app.collage.inc/typesense/search
363
+ ```
364
+
365
+ A 200 confirms the proxy is up. If it's down, the audit prompt's
366
+ fallback path keeps small-workspace flows working; large-workspace
367
+ audits return a structured finding instead.
368
+
369
+ ### "All endpoints return 402 'Workspace not available'"
370
+
371
+ Confirm `COLLAGE_WORKSPACE_ID` matches the workspace the
372
+ service-account user actually belongs to. The 402 error is a Collage
373
+ middleware message that fires when the user-token / workspace-id pair
374
+ doesn't resolve. Cross-check by hitting `check-workspace-access`
375
+ directly:
376
+
377
+ ```bash
378
+ curl -sS -H "Authorization: Bearer $COLLAGE_API_KEY" \
379
+ "https://damapi.collage.inc/api/v1/digital-assets/check-workspace-access?url_workspace_id=$COLLAGE_WORKSPACE_ID&workspace_id=$COLLAGE_WORKSPACE_ID"
380
+ ```
381
+
382
+ ### "Dry-run executes return CONFIRMATION_STATE_DIVERGED"
383
+
384
+ Spike in this means a concurrent mutator is touching the same
385
+ targets. Either:
386
+
387
+ 1. Another MCP client is operating on the same workspace.
388
+ 2. A human user is editing in the Collage UI in parallel.
389
+ 3. A scheduled job is mutating in the background.
390
+
391
+ The error's `cause.prior_plan` / `cause.new_plan` shows what
392
+ diverged. Resolve by re-prompting the LLM to re-preview, or by
393
+ serializing access at the client layer.
394
+
395
+ ### "Server crashes at startup"
396
+
397
+ Almost always env-related. The Zod-validated env loader fails loudly
398
+ with a specific missing variable. Check logs for
399
+ `Missing required environment variable: <NAME>`.
400
+
401
+ If the error mentions `LOG_LEVEL` rejection, you set an invalid
402
+ value (must be one of `debug`/`info`/`warn`/`error`).
403
+
404
+ If the error mentions `pino.transport`, that's a logger config
405
+ problem — fall back to `LOG_LEVEL=info` and check Node version
406
+ (>= 20).
407
+
408
+ ---
409
+
410
+ ## Update / upgrade procedure
411
+
412
+ The server follows [Semantic Versioning](https://semver.org). See
413
+ [`CHANGELOG.md`](../CHANGELOG.md) for the per-version delta.
414
+
415
+ Standard upgrade flow (after Collage takes over the publish credential):
416
+
417
+ ```bash
418
+ # (in the deployment repo)
419
+ npm install @collage-dam/mcp-server@latest
420
+ # or pin: npm install @collage-dam/mcp-server@0.2.0
421
+ ```
422
+
423
+ Then restart the server process. Breaking changes will be flagged in
424
+ the changelog and bump the major version (e.g. `0.x` → `1.0`).
425
+
426
+ The MCP protocol itself is versioned via the `initialize` capability
427
+ exchange. The server pins to `@modelcontextprotocol/sdk@1.12.1` —
428
+ upgrade SDK separately when the protocol bumps.
429
+
430
+ ---
431
+
432
+ ## Handoff checklist (for Collage ops)
433
+
434
+ When taking ownership at Week 6:
435
+
436
+ - [ ] Service-account user provisioned and JWT extracted.
437
+ - [ ] `COLLAGE_API_KEY`, `COLLAGE_WORKSPACE_ID`, `MCP_CONFIRMATION_SECRET` set in your secret manager.
438
+ - [ ] Smoke test: `npm run smoke` against the configured workspace returns `has_access: true` for `list_workspaces`.
439
+ - [ ] Logs are flowing to your aggregator (Datadog / CloudWatch / Loki).
440
+ - [ ] If using streamable-HTTP: Redis provisioned and reachable; TLS terminator configured.
441
+ - [ ] Token-rotation runbook entry added to your ops wiki, referencing this doc.
442
+ - [ ] Internal SLO defined for tool error rate (suggested: < 5% per tool, p95 latency < 3s).
443
+ - [ ] Alert on prolonged `UPSTREAM` rate (suggests Collage REST or search proxy is degraded).
444
+
445
+ For questions during transition: see the contacts list in
446
+ [`docs/api-field-verification.md`](./api-field-verification.md) header.
@@ -0,0 +1,195 @@
1
+ # Security Review — collage-mcp v0.1.0
2
+
3
+ **Reviewed:** 2026-05-05
4
+ **Scope:** `mvp-release-distribution` T6 — token storage, telemetry-by-default, redaction verification
5
+ **Reviewer:** Southleft (Justin)
6
+ **Outcome:** No P0 blockers. Three small defensive hardenings landed during the review (see _Changes Landed_); residual recommendations tracked below.
7
+
8
+ ---
9
+
10
+ ## Executive summary
11
+
12
+ The server is a stdio-first MCP server with one upstream destination (Collage REST API at `damapi.collage.inc`) and one search proxy (`app.collage.inc/typesense/search`). It ships no telemetry, no analytics SDKs, no postinstall scripts, and writes logs only to `stderr`. Secret material — the Collage Bearer JWT and the HMAC confirmation secret — is held in process memory, never persisted to disk by this server, and redacted in serialised config. The dry-run framework's confirmation tokens are HMAC-SHA256 signed, single-use (atomic `getAndDelete`), tool-name-bound, and fingerprint-bound, so a leaked token cannot be replayed against substituted args.
13
+
14
+ Three small defense-in-depth fixes were applied during the review:
15
+
16
+ 1. Pino redaction paths extended to cover `Authorization`, `bearer`, `signed_token`, `confirmation_token`, `apiKey`.
17
+ 2. Upstream error bodies (REST + search proxy) are now passed through a sanitiser that replaces `Bearer ...`, JWT-shaped strings, and `api_key=...` patterns before being interpolated into the LLM-visible `McpToolError.message`, then clamped to 512 chars.
18
+ 3. `loadConfig()` warns on stderr when `MCP_CONFIRMATION_SECRET` is shorter than 32 characters.
19
+
20
+ 5 new unit tests cover the sanitiser + entropy warning. Full suite: **523 passing** (up from 518), `tsc --noEmit` clean.
21
+
22
+ ---
23
+
24
+ ## Surface inventory
25
+
26
+ | Surface | Inbound | Outbound | Storage |
27
+ |---|---|---|---|
28
+ | stdio transport | JSON-RPC over stdin | JSON-RPC over stdout (framing only) | none |
29
+ | streamable-HTTP transport (Phase 2 / optional) | HTTPS (Collage-fronted) | (same) | Redis (ephemeral plans, TTL 60m) |
30
+ | REST client | n/a | `https://damapi.collage.inc/api/v1/*` (Bearer JWT) | none |
31
+ | Search proxy | n/a | `https://app.collage.inc/typesense/search` (Bearer JWT) | none |
32
+ | Logger | n/a | `stderr` (fd 2) only | none |
33
+
34
+ No other network egress, file I/O, or third-party services are touched.
35
+
36
+ ---
37
+
38
+ ## Findings
39
+
40
+ ### F1 — Upstream error bodies could pass through to LLM-visible messages — **resolved 2026-05-05**
41
+
42
+ `client.ts`'s `request()` and `typesense.ts`'s `postJson()` both wrapped the raw upstream response body into the thrown error's `message`, which then ends up in `McpToolError.message` returned to the calling LLM. Production Collage responses don't echo credentials, but defensive coding warranted a sanitiser at the boundary.
43
+
44
+ **Resolution:** added `sanitizeUpstreamBody()` (REST) and `sanitizeProxyBody()` (search) that:
45
+
46
+ - Replace `Bearer <token>` → `Bearer [REDACTED]`
47
+ - Replace JWT-shaped substrings (`eyJ...`) → `[REDACTED_JWT]`
48
+ - Replace `api_key=`, `token=`, `secret=` value pairs → `…=[REDACTED]`
49
+ - Clamp to 512 chars
50
+
51
+ Tests: `tests/unit/client.test.ts` — three new cases for Bearer, JWT, and length clamp.
52
+
53
+ **Severity:** medium (defense in depth, no known active leak path)
54
+ **Status:** ✅ fixed in this review
55
+
56
+ ---
57
+
58
+ ### F2 — Pino redact paths missed common secret-bearing keys — **resolved 2026-05-05**
59
+
60
+ The redaction path list (`*.api_key`, `*.token`, `*.password`, `*.secret`, `*.COLLAGE_API_KEY`, `*.TYPESENSE_API_KEY`, `*.MCP_CONFIRMATION_SECRET`) didn't cover `Authorization`, `bearer`, `apiKey` (camelCase), `signed_token`, or `confirmation_token`. No current call site logs these keys, but the redact rules are defense-in-depth — a future contributor could add a log line that does.
61
+
62
+ **Resolution:** redact path list extended in `src/conventions/logger.ts`. Existing `secrets-audit.test.ts` continues to pass; the new keys are additive.
63
+
64
+ **Severity:** low (no active leak; widens redaction net for future code)
65
+ **Status:** ✅ fixed in this review
66
+
67
+ ---
68
+
69
+ ### F3 — `MCP_CONFIRMATION_SECRET` strength only validated as `min(1)` — **resolved 2026-05-05**
70
+
71
+ `loadConfig()` accepted any non-empty string for `MCP_CONFIRMATION_SECRET`. A short secret (e.g., `"dev"`) would let HMAC-SHA256 confirmation tokens be brute-forced offline given a single observed token.
72
+
73
+ **Resolution:** `loadConfig()` now writes a warning to stderr when the secret is shorter than 32 chars:
74
+
75
+ ```
76
+ WARN: MCP_CONFIRMATION_SECRET is shorter than 32 characters. Use at least 32
77
+ high-entropy bytes (e.g. `openssl rand -hex 32`) so HMAC tokens are not
78
+ feasibly forgeable.
79
+ ```
80
+
81
+ This is a warning rather than a hard fail because the runbook recommendations already cover this and an existing deployment with a 24-char secret shouldn't refuse to start on upgrade. The hard-fail path stays available if the secret is missing entirely.
82
+
83
+ Tests: `tests/unit/conventions/env.test.ts` — two new cases (short secret warns, 32+ char secret silent).
84
+
85
+ **Severity:** medium
86
+ **Status:** ✅ fixed in this review
87
+
88
+ ---
89
+
90
+ ### F4 — Confirmation token TTL increased from 5m → 15m on 2026-05-01 — **information**
91
+
92
+ This is recorded here for the security review record because it widens the replay window for an exfiltrated token. The increase was deliberate (live QA showed 5m was hostile to human review pauses). The mitigations remain:
93
+
94
+ - HMAC-SHA256 with timing-safe comparison (`verifyConfirmation` uses `crypto.timingSafeEqual`)
95
+ - Single-use enforcement via atomic `EphemeralStateStore.getAndDelete`
96
+ - Tool-name binding (token bound to one tool; cross-tool reuse rejected)
97
+ - Fingerprint binding (re-plan + fingerprint compare on soft-expired tokens; mismatch returns `CONFIRMATION_STATE_DIVERGED`)
98
+ - 60m hard memo TTL (after which the plan record is gone and the token is dead)
99
+
100
+ Net replay-window effect: the soft window grew from 5m → 15m, but the token still cannot be reused with substituted args, cannot be reused after one execute, and cannot survive the hard 60m memo TTL.
101
+
102
+ **Severity:** low (compensating controls intact)
103
+ **Status:** noted; no change needed
104
+
105
+ ---
106
+
107
+ ### F5 — REST client logger logs the upstream error message at WARN level — **information**
108
+
109
+ `client.ts:1292` logs `{err: {name, message}}` on every failed request. The message originates from the upstream body and is now sanitised (F1), so the log is bounded and credential-stripped. Stays as a recommendation if Collage operations decide to forward stderr to a remote sink — they should treat the JSON `err.message` field as `internal-debug` and not customer-visible.
110
+
111
+ **Severity:** informational
112
+ **Status:** documented in `docs/deployment-runbook.md`'s "Logging conventions" section
113
+
114
+ ---
115
+
116
+ ## Verified clean (no action)
117
+
118
+ Things checked and found acceptable as-is:
119
+
120
+ - **No telemetry SDKs.** Dependency tree: `@modelcontextprotocol/sdk`, `ioredis`, `pino`, `ulid`, `zod`. No analytics, no error reporting, no usage beacons.
121
+ - **No postinstall / preinstall scripts.** Reduces supply-chain risk on `npm install`.
122
+ - **No outbound HTTP except Collage-controlled endpoints.** Verified by grep: only `globalThis.fetch` calls in `client.ts` (REST) and `typesense.ts` (search proxy).
123
+ - **Logger writes to stderr only** (`pino.destination({ fd: 2 })`). Stdout is reserved for MCP framing.
124
+ - **Bearer header constructed inline at request time.** Never logged. The `headers` object is not interpolated into any log line.
125
+ - **HMAC confirmation flow.** Timing-safe comparison via `crypto.timingSafeEqual`, single-use via atomic `getAndDelete`, tool-name binding, fingerprint binding. See `src/conventions/confirmation.ts` and `src/conventions/dry-run/with-dry-run.ts`.
126
+ - **AppConfig redacts via `toJSON()` / `toString()`.** Direct property access still returns the real value (so the client can use the token), but anything that JSON-serialises the config — including accidental log lines or error.cause objects — gets `[REDACTED]`. See `src/conventions/env.ts:withRedaction()`.
127
+ - **npm tarball is tight.** `files` allowlist scopes the package to `dist`, `README.md`, `CHANGELOG.md`, `LICENSE`, `.env.example`, `docs`. `npm pack --dry-run` confirmed no `.env`, no tests, no source maps from outside `dist`. 261 files, 228KB packed.
128
+ - **`.env` excluded from git.** `.gitignore` covers `*.env` with a `!.env.example` un-ignore. No `.env` checked in.
129
+ - **No hardcoded secrets in `src/`.** Existing `tests/unit/conventions/secrets-audit.test.ts` greps source for OpenAI-style keys, hardcoded Bearer tokens, and `api_key="..."` / `token="..."` patterns. Passes.
130
+ - **`.env.example` has empty placeholders.** Tested (`secrets-audit.test.ts`). No real secrets shipped.
131
+ - **README + runbook cover token guidance.** Service-account user, JWT extraction steps, rotation cadence, secret-manager guidance, Confirmation secret rotation as a kill switch.
132
+ - **Search proxy is enforcement seam.** Workspace scope is enforced server-side by the Nuxt `/typesense/search` middleware (it verifies the Bearer via Laravel `/user`, resolves the caller's `workspace_unique_id`, and rewrites the filter). The MCP wrapper cannot construct a cross-workspace query because the proxy refuses any workspace the Bearer's user is not a member of. Documented in `src/typesense.ts` and the deployment runbook.
133
+ - **Rate limiting present.** `TokenBucketRateLimiter` (default 10 RPS, env-tunable) caps egress to upstream. Not a security control directly, but bounds blast radius of a runaway loop.
134
+
135
+ ---
136
+
137
+ ## Telemetry / data-egress posture
138
+
139
+ This server, by design, has **no telemetry**. There is no opt-in flag because there is nothing to opt into:
140
+
141
+ - No usage events fired on tool invocation.
142
+ - No error reporting to a remote SaaS (Sentry, etc.) — errors flow only to stderr and back to the calling LLM.
143
+ - No "anonymous metrics" emitted on startup or shutdown.
144
+
145
+ The deliverable explicitly meets the spec's "no telemetry by default, opt-in error reporting" requirement by virtue of having no telemetry surface at all. If Collage wants to add error reporting post-handoff, it should be a separate, opt-in module wired into `src/conventions/logger.ts`'s pino transport stack — recommended approach: an additional pino transport target gated behind an env var like `COLLAGE_MCP_ERROR_REPORTING_DSN`, default-empty.
146
+
147
+ ---
148
+
149
+ ## Token storage posture
150
+
151
+ Two secrets are loaded at startup via `loadConfig()`:
152
+
153
+ 1. `COLLAGE_API_KEY` — JWT Bearer token. Held as a `private readonly` field on `CollageClient`. Used as the `Authorization: Bearer …` header on every Collage REST and search-proxy request. Never written to disk. Never logged (verified by grepping all log call sites).
154
+ 2. `MCP_CONFIRMATION_SECRET` — HMAC signing secret. Held by reference in each mutating tool's `MutatingToolDeps.secret`. Used only in `crypto.createHmac('sha256', secret)` calls. Never written to disk. Never logged.
155
+
156
+ Both are redacted from `AppConfig`'s `toJSON()` / `toString()` output. The redacted-config defense covers the failure mode where a future contributor logs the full config struct.
157
+
158
+ Operations guidance (see `docs/deployment-runbook.md`):
159
+
160
+ - Use a dedicated service-account user for the JWT, not a personal session.
161
+ - Rotate the JWT when the upstream session expires (currently no refresh-token flow on the Collage side).
162
+ - Generate `MCP_CONFIRMATION_SECRET` with `openssl rand -hex 32` minimum.
163
+ - Rotating the confirmation secret invalidates every in-flight confirmation token by design — that is the intended kill switch for a stale-plan incident.
164
+
165
+ ---
166
+
167
+ ## Changes landed in this review
168
+
169
+ | File | Change |
170
+ |---|---|
171
+ | `src/conventions/logger.ts` | Extended pino redact paths (Authorization, bearer, signed_token, confirmation_token, apiKey) |
172
+ | `src/conventions/env.ts` | Stderr warning when `MCP_CONFIRMATION_SECRET.length < 32` |
173
+ | `src/client.ts` | Added `sanitizeUpstreamBody()` — Bearer/JWT/`*=...` scrub + 512-char clamp before upstream body reaches `McpToolError.message` |
174
+ | `src/typesense.ts` | Mirror of the same sanitiser at the search-proxy boundary |
175
+ | `tests/unit/conventions/env.test.ts` | 2 new tests (short-secret warn, long-secret silent) |
176
+ | `tests/unit/client.test.ts` | 3 new tests (Bearer scrub, JWT scrub, length clamp) |
177
+
178
+ Tests: 523 passing (up from 518). Lint: clean.
179
+
180
+ ---
181
+
182
+ ## Recommendations for Collage post-handoff
183
+
184
+ Not blocking the v0.1.0 ship, but worth tracking:
185
+
186
+ 1. **Service-account JWT refresh.** Today the JWT expires with the upstream session. If Collage adds a refresh-token endpoint server-side, wire it into `CollageClient` so long-lived deployments don't require manual rotation. (Tracked in runbook secret-rotation section.)
187
+ 2. **Optional opt-in error reporting.** If/when Collage wants Sentry-like error visibility, add a pino transport target gated on a `COLLAGE_MCP_ERROR_REPORTING_DSN` env var. Default empty → no transport → current behavior unchanged.
188
+ 3. **Hard-fail mode for short confirmation secrets.** Current behavior is a warning; some Collage deployments may prefer hard-fail. Consider a `MCP_STRICT_SECRETS=1` env that turns the warning into a thrown error.
189
+ 4. **Audit log for executed mutations.** Every mutation runs through the dry-run framework and produces an execute-phase ledger. If Collage wants a tamper-evident audit trail beyond stderr logs, persist the ledger plus the confirmation_id and request_id to the same Redis used for the plan store, with a longer TTL or a separate sink.
190
+
191
+ ---
192
+
193
+ ## Sign-off
194
+
195
+ The v0.1.0 deliverable meets the SOW Phase 2 security-review requirement. No P0 blockers. The three medium-severity items identified during the review (F1, F2, F3) have been resolved in-band. The two informational items (F4, F5) are documented and have intact compensating controls. Recommended for ship.