@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.
- package/.env.example +56 -0
- package/CHANGELOG.md +90 -0
- package/LICENSE +21 -0
- package/README.md +512 -0
- package/dist/client.d.ts +497 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +1162 -0
- package/dist/client.js.map +1 -0
- package/dist/conventions/confirmation.d.ts +89 -0
- package/dist/conventions/confirmation.d.ts.map +1 -0
- package/dist/conventions/confirmation.js +132 -0
- package/dist/conventions/confirmation.js.map +1 -0
- package/dist/conventions/dry-run/batch-executor.d.ts +36 -0
- package/dist/conventions/dry-run/batch-executor.d.ts.map +1 -0
- package/dist/conventions/dry-run/batch-executor.js +89 -0
- package/dist/conventions/dry-run/batch-executor.js.map +1 -0
- package/dist/conventions/dry-run/diff-renderer.d.ts +34 -0
- package/dist/conventions/dry-run/diff-renderer.d.ts.map +1 -0
- package/dist/conventions/dry-run/diff-renderer.js +158 -0
- package/dist/conventions/dry-run/diff-renderer.js.map +1 -0
- package/dist/conventions/dry-run/index.d.ts +13 -0
- package/dist/conventions/dry-run/index.d.ts.map +1 -0
- package/dist/conventions/dry-run/index.js +10 -0
- package/dist/conventions/dry-run/index.js.map +1 -0
- package/dist/conventions/dry-run/mutating-tool.d.ts +64 -0
- package/dist/conventions/dry-run/mutating-tool.d.ts.map +1 -0
- package/dist/conventions/dry-run/mutating-tool.js +88 -0
- package/dist/conventions/dry-run/mutating-tool.js.map +1 -0
- package/dist/conventions/dry-run/summary.d.ts +66 -0
- package/dist/conventions/dry-run/summary.d.ts.map +1 -0
- package/dist/conventions/dry-run/summary.js +185 -0
- package/dist/conventions/dry-run/summary.js.map +1 -0
- package/dist/conventions/dry-run/types.d.ts +597 -0
- package/dist/conventions/dry-run/types.d.ts.map +1 -0
- package/dist/conventions/dry-run/types.js +108 -0
- package/dist/conventions/dry-run/types.js.map +1 -0
- package/dist/conventions/dry-run/with-dry-run.d.ts +66 -0
- package/dist/conventions/dry-run/with-dry-run.d.ts.map +1 -0
- package/dist/conventions/dry-run/with-dry-run.js +219 -0
- package/dist/conventions/dry-run/with-dry-run.js.map +1 -0
- package/dist/conventions/env.d.ts +49 -0
- package/dist/conventions/env.d.ts.map +1 -0
- package/dist/conventions/env.js +84 -0
- package/dist/conventions/env.js.map +1 -0
- package/dist/conventions/errors.d.ts +68 -0
- package/dist/conventions/errors.d.ts.map +1 -0
- package/dist/conventions/errors.js +81 -0
- package/dist/conventions/errors.js.map +1 -0
- package/dist/conventions/logger.d.ts +28 -0
- package/dist/conventions/logger.d.ts.map +1 -0
- package/dist/conventions/logger.js +105 -0
- package/dist/conventions/logger.js.map +1 -0
- package/dist/conventions/pagination.d.ts +37 -0
- package/dist/conventions/pagination.d.ts.map +1 -0
- package/dist/conventions/pagination.js +53 -0
- package/dist/conventions/pagination.js.map +1 -0
- package/dist/conventions/rate-limiter.d.ts +54 -0
- package/dist/conventions/rate-limiter.d.ts.map +1 -0
- package/dist/conventions/rate-limiter.js +143 -0
- package/dist/conventions/rate-limiter.js.map +1 -0
- package/dist/conventions/response-budget.d.ts +66 -0
- package/dist/conventions/response-budget.d.ts.map +1 -0
- package/dist/conventions/response-budget.js +89 -0
- package/dist/conventions/response-budget.js.map +1 -0
- package/dist/conventions/schema-version.d.ts +27 -0
- package/dist/conventions/schema-version.d.ts.map +1 -0
- package/dist/conventions/schema-version.js +29 -0
- package/dist/conventions/schema-version.js.map +1 -0
- package/dist/conventions/state-store-redis.d.ts +32 -0
- package/dist/conventions/state-store-redis.d.ts.map +1 -0
- package/dist/conventions/state-store-redis.js +77 -0
- package/dist/conventions/state-store-redis.js.map +1 -0
- package/dist/conventions/state-store.d.ts +46 -0
- package/dist/conventions/state-store.d.ts.map +1 -0
- package/dist/conventions/state-store.js +105 -0
- package/dist/conventions/state-store.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +421 -0
- package/dist/index.js.map +1 -0
- package/dist/prompts/collection-audit.d.ts +13 -0
- package/dist/prompts/collection-audit.d.ts.map +1 -0
- package/dist/prompts/collection-audit.js +168 -0
- package/dist/prompts/collection-audit.js.map +1 -0
- package/dist/prompts/create-distribution.d.ts +15 -0
- package/dist/prompts/create-distribution.d.ts.map +1 -0
- package/dist/prompts/create-distribution.js +111 -0
- package/dist/prompts/create-distribution.js.map +1 -0
- package/dist/prompts/helpers.d.ts +20 -0
- package/dist/prompts/helpers.d.ts.map +1 -0
- package/dist/prompts/helpers.js +53 -0
- package/dist/prompts/helpers.js.map +1 -0
- package/dist/prompts/library-health-audit.d.ts +13 -0
- package/dist/prompts/library-health-audit.d.ts.map +1 -0
- package/dist/prompts/library-health-audit.js +131 -0
- package/dist/prompts/library-health-audit.js.map +1 -0
- package/dist/prompts/usage-insights.d.ts +13 -0
- package/dist/prompts/usage-insights.d.ts.map +1 -0
- package/dist/prompts/usage-insights.js +98 -0
- package/dist/prompts/usage-insights.js.map +1 -0
- package/dist/prompts/wrap-prompt-as-tool.d.ts +48 -0
- package/dist/prompts/wrap-prompt-as-tool.d.ts.map +1 -0
- package/dist/prompts/wrap-prompt-as-tool.js +61 -0
- package/dist/prompts/wrap-prompt-as-tool.js.map +1 -0
- package/dist/resources/asset-by-id.d.ts +4 -0
- package/dist/resources/asset-by-id.d.ts.map +1 -0
- package/dist/resources/asset-by-id.js +27 -0
- package/dist/resources/asset-by-id.js.map +1 -0
- package/dist/resources/collections.d.ts +5 -0
- package/dist/resources/collections.d.ts.map +1 -0
- package/dist/resources/collections.js +48 -0
- package/dist/resources/collections.js.map +1 -0
- package/dist/resources/custom-fields.d.ts +4 -0
- package/dist/resources/custom-fields.d.ts.map +1 -0
- package/dist/resources/custom-fields.js +30 -0
- package/dist/resources/custom-fields.js.map +1 -0
- package/dist/resources/folders.d.ts +5 -0
- package/dist/resources/folders.d.ts.map +1 -0
- package/dist/resources/folders.js +73 -0
- package/dist/resources/folders.js.map +1 -0
- package/dist/resources/helpers.d.ts +17 -0
- package/dist/resources/helpers.d.ts.map +1 -0
- package/dist/resources/helpers.js +59 -0
- package/dist/resources/helpers.js.map +1 -0
- package/dist/resources/portals.d.ts +5 -0
- package/dist/resources/portals.d.ts.map +1 -0
- package/dist/resources/portals.js +81 -0
- package/dist/resources/portals.js.map +1 -0
- package/dist/resources/recent-and-dashboard.d.ts +5 -0
- package/dist/resources/recent-and-dashboard.d.ts.map +1 -0
- package/dist/resources/recent-and-dashboard.js +42 -0
- package/dist/resources/recent-and-dashboard.js.map +1 -0
- package/dist/tools/asset-selection.d.ts +102 -0
- package/dist/tools/asset-selection.d.ts.map +1 -0
- package/dist/tools/asset-selection.js +133 -0
- package/dist/tools/asset-selection.js.map +1 -0
- package/dist/tools/audit/audit-folder-structure.d.ts +108 -0
- package/dist/tools/audit/audit-folder-structure.d.ts.map +1 -0
- package/dist/tools/audit/audit-folder-structure.js +260 -0
- package/dist/tools/audit/audit-folder-structure.js.map +1 -0
- package/dist/tools/audit/audit-naming-conventions.d.ts +83 -0
- package/dist/tools/audit/audit-naming-conventions.d.ts.map +1 -0
- package/dist/tools/audit/audit-naming-conventions.js +238 -0
- package/dist/tools/audit/audit-naming-conventions.js.map +1 -0
- package/dist/tools/audit/audit-tagging-hygiene.d.ts +77 -0
- package/dist/tools/audit/audit-tagging-hygiene.d.ts.map +1 -0
- package/dist/tools/audit/audit-tagging-hygiene.js +402 -0
- package/dist/tools/audit/audit-tagging-hygiene.js.map +1 -0
- package/dist/tools/audit/detect-duplicates.d.ts +62 -0
- package/dist/tools/audit/detect-duplicates.d.ts.map +1 -0
- package/dist/tools/audit/detect-duplicates.js +0 -0
- package/dist/tools/audit/detect-duplicates.js.map +1 -0
- package/dist/tools/audit/types.d.ts +526 -0
- package/dist/tools/audit/types.d.ts.map +1 -0
- package/dist/tools/audit/types.js +188 -0
- package/dist/tools/audit/types.js.map +1 -0
- package/dist/tools/bulk-move-assets.d.ts +78 -0
- package/dist/tools/bulk-move-assets.d.ts.map +1 -0
- package/dist/tools/bulk-move-assets.js +122 -0
- package/dist/tools/bulk-move-assets.js.map +1 -0
- package/dist/tools/bulk-normalize-filenames.d.ts +62 -0
- package/dist/tools/bulk-normalize-filenames.d.ts.map +1 -0
- package/dist/tools/bulk-normalize-filenames.js +237 -0
- package/dist/tools/bulk-normalize-filenames.js.map +1 -0
- package/dist/tools/bulk-rename-assets.d.ts +79 -0
- package/dist/tools/bulk-rename-assets.d.ts.map +1 -0
- package/dist/tools/bulk-rename-assets.js +139 -0
- package/dist/tools/bulk-rename-assets.js.map +1 -0
- package/dist/tools/bulk-tags.d.ts +107 -0
- package/dist/tools/bulk-tags.d.ts.map +1 -0
- package/dist/tools/bulk-tags.js +220 -0
- package/dist/tools/bulk-tags.js.map +1 -0
- package/dist/tools/client-adapters.d.ts +76 -0
- package/dist/tools/client-adapters.d.ts.map +1 -0
- package/dist/tools/client-adapters.js +648 -0
- package/dist/tools/client-adapters.js.map +1 -0
- package/dist/tools/collection-membership.d.ts +90 -0
- package/dist/tools/collection-membership.d.ts.map +1 -0
- package/dist/tools/collection-membership.js +195 -0
- package/dist/tools/collection-membership.js.map +1 -0
- package/dist/tools/create-collection.d.ts +63 -0
- package/dist/tools/create-collection.d.ts.map +1 -0
- package/dist/tools/create-collection.js +151 -0
- package/dist/tools/create-collection.js.map +1 -0
- package/dist/tools/create-folder.d.ts +46 -0
- package/dist/tools/create-folder.d.ts.map +1 -0
- package/dist/tools/create-folder.js +83 -0
- package/dist/tools/create-folder.js.map +1 -0
- package/dist/tools/create-share-link.d.ts +107 -0
- package/dist/tools/create-share-link.d.ts.map +1 -0
- package/dist/tools/create-share-link.js +239 -0
- package/dist/tools/create-share-link.js.map +1 -0
- package/dist/tools/get-asset-details.d.ts +401 -0
- package/dist/tools/get-asset-details.d.ts.map +1 -0
- package/dist/tools/get-asset-details.js +56 -0
- package/dist/tools/get-asset-details.js.map +1 -0
- package/dist/tools/get-collection.d.ts +126 -0
- package/dist/tools/get-collection.d.ts.map +1 -0
- package/dist/tools/get-collection.js +52 -0
- package/dist/tools/get-collection.js.map +1 -0
- package/dist/tools/get-embed-code.d.ts +195 -0
- package/dist/tools/get-embed-code.d.ts.map +1 -0
- package/dist/tools/get-embed-code.js +214 -0
- package/dist/tools/get-embed-code.js.map +1 -0
- package/dist/tools/insights/analyze-share-links.d.ts +159 -0
- package/dist/tools/insights/analyze-share-links.d.ts.map +1 -0
- package/dist/tools/insights/analyze-share-links.js +314 -0
- package/dist/tools/insights/analyze-share-links.js.map +1 -0
- package/dist/tools/insights/insight-cache.d.ts +36 -0
- package/dist/tools/insights/insight-cache.d.ts.map +1 -0
- package/dist/tools/insights/insight-cache.js +98 -0
- package/dist/tools/insights/insight-cache.js.map +1 -0
- package/dist/tools/insights/report-asset-activation.d.ts +149 -0
- package/dist/tools/insights/report-asset-activation.d.ts.map +1 -0
- package/dist/tools/insights/report-asset-activation.js +380 -0
- package/dist/tools/insights/report-asset-activation.js.map +1 -0
- package/dist/tools/insights/report-stale-assets.d.ts +120 -0
- package/dist/tools/insights/report-stale-assets.d.ts.map +1 -0
- package/dist/tools/insights/report-stale-assets.js +281 -0
- package/dist/tools/insights/report-stale-assets.js.map +1 -0
- package/dist/tools/insights/report-top-assets.d.ts +139 -0
- package/dist/tools/insights/report-top-assets.d.ts.map +1 -0
- package/dist/tools/insights/report-top-assets.js +407 -0
- package/dist/tools/insights/report-top-assets.js.map +1 -0
- package/dist/tools/list-categories.d.ts +127 -0
- package/dist/tools/list-categories.d.ts.map +1 -0
- package/dist/tools/list-categories.js +68 -0
- package/dist/tools/list-categories.js.map +1 -0
- package/dist/tools/list-collections.d.ts +127 -0
- package/dist/tools/list-collections.d.ts.map +1 -0
- package/dist/tools/list-collections.js +53 -0
- package/dist/tools/list-collections.js.map +1 -0
- package/dist/tools/list-custom-fields.d.ts +125 -0
- package/dist/tools/list-custom-fields.d.ts.map +1 -0
- package/dist/tools/list-custom-fields.js +51 -0
- package/dist/tools/list-custom-fields.js.map +1 -0
- package/dist/tools/list-share-links.d.ts +192 -0
- package/dist/tools/list-share-links.d.ts.map +1 -0
- package/dist/tools/list-share-links.js +92 -0
- package/dist/tools/list-share-links.js.map +1 -0
- package/dist/tools/list-workspaces.d.ts +88 -0
- package/dist/tools/list-workspaces.d.ts.map +1 -0
- package/dist/tools/list-workspaces.js +71 -0
- package/dist/tools/list-workspaces.js.map +1 -0
- package/dist/tools/move-asset.d.ts +48 -0
- package/dist/tools/move-asset.d.ts.map +1 -0
- package/dist/tools/move-asset.js +85 -0
- package/dist/tools/move-asset.js.map +1 -0
- package/dist/tools/rename-asset.d.ts +88 -0
- package/dist/tools/rename-asset.d.ts.map +1 -0
- package/dist/tools/rename-asset.js +100 -0
- package/dist/tools/rename-asset.js.map +1 -0
- package/dist/tools/rename-folder.d.ts +55 -0
- package/dist/tools/rename-folder.d.ts.map +1 -0
- package/dist/tools/rename-folder.js +101 -0
- package/dist/tools/rename-folder.js.map +1 -0
- package/dist/tools/revoke-share-link.d.ts +55 -0
- package/dist/tools/revoke-share-link.d.ts.map +1 -0
- package/dist/tools/revoke-share-link.js +77 -0
- package/dist/tools/revoke-share-link.js.map +1 -0
- package/dist/tools/search/facets.d.ts +34 -0
- package/dist/tools/search/facets.d.ts.map +1 -0
- package/dist/tools/search/facets.js +147 -0
- package/dist/tools/search/facets.js.map +1 -0
- package/dist/tools/search/filter-builder.d.ts +33 -0
- package/dist/tools/search/filter-builder.d.ts.map +1 -0
- package/dist/tools/search/filter-builder.js +111 -0
- package/dist/tools/search/filter-builder.js.map +1 -0
- package/dist/tools/search/search-assets.d.ts +41 -0
- package/dist/tools/search/search-assets.d.ts.map +1 -0
- package/dist/tools/search/search-assets.js +162 -0
- package/dist/tools/search/search-assets.js.map +1 -0
- package/dist/tools/search/search-collections.d.ts +35 -0
- package/dist/tools/search/search-collections.d.ts.map +1 -0
- package/dist/tools/search/search-collections.js +103 -0
- package/dist/tools/search/search-collections.js.map +1 -0
- package/dist/tools/search/types.d.ts +1047 -0
- package/dist/tools/search/types.d.ts.map +1 -0
- package/dist/tools/search/types.js +216 -0
- package/dist/tools/search/types.js.map +1 -0
- package/dist/tools/update-asset-metadata.d.ts +78 -0
- package/dist/tools/update-asset-metadata.d.ts.map +1 -0
- package/dist/tools/update-asset-metadata.js +203 -0
- package/dist/tools/update-asset-metadata.js.map +1 -0
- package/dist/tools/update-collection.d.ts +69 -0
- package/dist/tools/update-collection.d.ts.map +1 -0
- package/dist/tools/update-collection.js +142 -0
- package/dist/tools/update-collection.js.map +1 -0
- package/dist/tools/view-category-contents.d.ts +231 -0
- package/dist/tools/view-category-contents.d.ts.map +1 -0
- package/dist/tools/view-category-contents.js +97 -0
- package/dist/tools/view-category-contents.js.map +1 -0
- package/dist/types.d.ts +1326 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +288 -0
- package/dist/types.js.map +1 -0
- package/dist/typesense.d.ts +84 -0
- package/dist/typesense.d.ts.map +1 -0
- package/dist/typesense.js +243 -0
- package/dist/typesense.js.map +1 -0
- package/docs/api-field-verification.md +244 -0
- package/docs/deployment-runbook.md +446 -0
- package/docs/security-review.md +195 -0
- package/docs/typesense-filter-schema.md +262 -0
- package/docs/verified-endpoints.md +38 -0
- 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.
|