@withpica/mcp-server 2.52.0 → 2.53.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (251) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/dist/prompts/creator-question-atlas.d.ts +48 -0
  3. package/dist/prompts/creator-question-atlas.d.ts.map +1 -0
  4. package/dist/prompts/creator-question-atlas.js +618 -0
  5. package/dist/prompts/creator-question-atlas.js.map +1 -0
  6. package/dist/prompts/index.d.ts +32 -0
  7. package/dist/prompts/index.d.ts.map +1 -1
  8. package/dist/prompts/index.js +235 -0
  9. package/dist/prompts/index.js.map +1 -1
  10. package/dist/resources/index.d.ts +10 -0
  11. package/dist/resources/index.d.ts.map +1 -1
  12. package/dist/resources/index.js +134 -1
  13. package/dist/resources/index.js.map +1 -1
  14. package/dist/server-instructions.d.ts +4 -3
  15. package/dist/server-instructions.d.ts.map +1 -1
  16. package/dist/server-instructions.js +4 -1
  17. package/dist/server-instructions.js.map +1 -1
  18. package/dist/server.d.ts +26 -0
  19. package/dist/server.d.ts.map +1 -1
  20. package/dist/server.js +108 -10
  21. package/dist/server.js.map +1 -1
  22. package/dist/skills/index.d.ts +42 -0
  23. package/dist/skills/index.d.ts.map +1 -0
  24. package/dist/skills/index.js +59 -0
  25. package/dist/skills/index.js.map +1 -0
  26. package/dist/skills/skills.generated.d.ts +25 -0
  27. package/dist/skills/skills.generated.d.ts.map +1 -0
  28. package/dist/skills/skills.generated.js +86 -0
  29. package/dist/skills/skills.generated.js.map +1 -0
  30. package/dist/tools/access-simulate.d.ts +23 -0
  31. package/dist/tools/access-simulate.d.ts.map +1 -0
  32. package/dist/tools/access-simulate.js +165 -0
  33. package/dist/tools/access-simulate.js.map +1 -0
  34. package/dist/tools/agent-identity.d.ts.map +1 -1
  35. package/dist/tools/agent-identity.js +15 -0
  36. package/dist/tools/agent-identity.js.map +1 -1
  37. package/dist/tools/agreement-types.d.ts.map +1 -1
  38. package/dist/tools/agreement-types.js +24 -0
  39. package/dist/tools/agreement-types.js.map +1 -1
  40. package/dist/tools/agreements.d.ts.map +1 -1
  41. package/dist/tools/agreements.js +21 -3
  42. package/dist/tools/agreements.js.map +1 -1
  43. package/dist/tools/analytics.d.ts.map +1 -1
  44. package/dist/tools/analytics.js +19 -1
  45. package/dist/tools/analytics.js.map +1 -1
  46. package/dist/tools/app-tools.d.ts.map +1 -1
  47. package/dist/tools/app-tools.js +11 -2
  48. package/dist/tools/app-tools.js.map +1 -1
  49. package/dist/tools/assets.d.ts.map +1 -1
  50. package/dist/tools/assets.js +33 -0
  51. package/dist/tools/assets.js.map +1 -1
  52. package/dist/tools/audio-files.d.ts +5 -0
  53. package/dist/tools/audio-files.d.ts.map +1 -1
  54. package/dist/tools/audio-files.js +91 -0
  55. package/dist/tools/audio-files.js.map +1 -1
  56. package/dist/tools/audit.d.ts.map +1 -1
  57. package/dist/tools/audit.js +11 -2
  58. package/dist/tools/audit.js.map +1 -1
  59. package/dist/tools/auth.d.ts.map +1 -1
  60. package/dist/tools/auth.js +6 -0
  61. package/dist/tools/auth.js.map +1 -1
  62. package/dist/tools/bulk.d.ts +4 -0
  63. package/dist/tools/bulk.d.ts.map +1 -1
  64. package/dist/tools/bulk.js +304 -0
  65. package/dist/tools/bulk.js.map +1 -1
  66. package/dist/tools/calendar.d.ts.map +1 -1
  67. package/dist/tools/calendar.js +3 -0
  68. package/dist/tools/calendar.js.map +1 -1
  69. package/dist/tools/collaborators.d.ts.map +1 -1
  70. package/dist/tools/collaborators.js +24 -3
  71. package/dist/tools/collaborators.js.map +1 -1
  72. package/dist/tools/comparisons.d.ts.map +1 -1
  73. package/dist/tools/comparisons.js +6 -0
  74. package/dist/tools/comparisons.js.map +1 -1
  75. package/dist/tools/credits.d.ts +18 -0
  76. package/dist/tools/credits.d.ts.map +1 -1
  77. package/dist/tools/credits.js +344 -4
  78. package/dist/tools/credits.js.map +1 -1
  79. package/dist/tools/custody.d.ts.map +1 -1
  80. package/dist/tools/custody.js +23 -2
  81. package/dist/tools/custody.js.map +1 -1
  82. package/dist/tools/dashboard.d.ts.map +1 -1
  83. package/dist/tools/dashboard.js +43 -7
  84. package/dist/tools/dashboard.js.map +1 -1
  85. package/dist/tools/directory.d.ts.map +1 -1
  86. package/dist/tools/directory.js +3 -0
  87. package/dist/tools/directory.js.map +1 -1
  88. package/dist/tools/discovery.d.ts.map +1 -1
  89. package/dist/tools/discovery.js +99 -2
  90. package/dist/tools/discovery.js.map +1 -1
  91. package/dist/tools/disputes.d.ts.map +1 -1
  92. package/dist/tools/disputes.js +4 -1
  93. package/dist/tools/disputes.js.map +1 -1
  94. package/dist/tools/documents.d.ts.map +1 -1
  95. package/dist/tools/documents.js +3 -0
  96. package/dist/tools/documents.js.map +1 -1
  97. package/dist/tools/duplicates.d.ts.map +1 -1
  98. package/dist/tools/duplicates.js +6 -0
  99. package/dist/tools/duplicates.js.map +1 -1
  100. package/dist/tools/enrichment.d.ts.map +1 -1
  101. package/dist/tools/enrichment.js +33 -0
  102. package/dist/tools/enrichment.js.map +1 -1
  103. package/dist/tools/explainability.d.ts +24 -0
  104. package/dist/tools/explainability.d.ts.map +1 -0
  105. package/dist/tools/explainability.js +137 -0
  106. package/dist/tools/explainability.js.map +1 -0
  107. package/dist/tools/exports.d.ts.map +1 -1
  108. package/dist/tools/exports.js +18 -3
  109. package/dist/tools/exports.js.map +1 -1
  110. package/dist/tools/feedback.d.ts.map +1 -1
  111. package/dist/tools/feedback.js +3 -0
  112. package/dist/tools/feedback.js.map +1 -1
  113. package/dist/tools/files.d.ts.map +1 -1
  114. package/dist/tools/files.js +22 -0
  115. package/dist/tools/files.js.map +1 -1
  116. package/dist/tools/groups.d.ts.map +1 -1
  117. package/dist/tools/groups.js +12 -0
  118. package/dist/tools/groups.js.map +1 -1
  119. package/dist/tools/import-documents.d.ts.map +1 -1
  120. package/dist/tools/import-documents.js +10 -1
  121. package/dist/tools/import-documents.js.map +1 -1
  122. package/dist/tools/import.d.ts.map +1 -1
  123. package/dist/tools/import.js +36 -3
  124. package/dist/tools/import.js.map +1 -1
  125. package/dist/tools/index.d.ts +142 -6
  126. package/dist/tools/index.d.ts.map +1 -1
  127. package/dist/tools/index.js +289 -108
  128. package/dist/tools/index.js.map +1 -1
  129. package/dist/tools/integrations.d.ts.map +1 -1
  130. package/dist/tools/integrations.js +28 -8
  131. package/dist/tools/integrations.js.map +1 -1
  132. package/dist/tools/labels.d.ts.map +1 -1
  133. package/dist/tools/labels.js +3 -0
  134. package/dist/tools/labels.js.map +1 -1
  135. package/dist/tools/licensing.d.ts.map +1 -1
  136. package/dist/tools/licensing.js +15 -0
  137. package/dist/tools/licensing.js.map +1 -1
  138. package/dist/tools/memory.d.ts.map +1 -1
  139. package/dist/tools/memory.js +15 -3
  140. package/dist/tools/memory.js.map +1 -1
  141. package/dist/tools/metadata.d.ts.map +1 -1
  142. package/dist/tools/metadata.js +112 -0
  143. package/dist/tools/metadata.js.map +1 -1
  144. package/dist/tools/multimedia.d.ts.map +1 -1
  145. package/dist/tools/multimedia.js +15 -0
  146. package/dist/tools/multimedia.js.map +1 -1
  147. package/dist/tools/my-recent-questions.d.ts +25 -0
  148. package/dist/tools/my-recent-questions.d.ts.map +1 -0
  149. package/dist/tools/my-recent-questions.js +186 -0
  150. package/dist/tools/my-recent-questions.js.map +1 -0
  151. package/dist/tools/my-reported-issues.d.ts.map +1 -1
  152. package/dist/tools/my-reported-issues.js +3 -0
  153. package/dist/tools/my-reported-issues.js.map +1 -1
  154. package/dist/tools/notes.d.ts.map +1 -1
  155. package/dist/tools/notes.js +12 -0
  156. package/dist/tools/notes.js.map +1 -1
  157. package/dist/tools/notifications.d.ts.map +1 -1
  158. package/dist/tools/notifications.js +25 -1
  159. package/dist/tools/notifications.js.map +1 -1
  160. package/dist/tools/onboarding.d.ts.map +1 -1
  161. package/dist/tools/onboarding.js +3 -0
  162. package/dist/tools/onboarding.js.map +1 -1
  163. package/dist/tools/people.d.ts +4 -0
  164. package/dist/tools/people.d.ts.map +1 -1
  165. package/dist/tools/people.js +58 -1
  166. package/dist/tools/people.js.map +1 -1
  167. package/dist/tools/projects.d.ts.map +1 -1
  168. package/dist/tools/projects.js +18 -0
  169. package/dist/tools/projects.js.map +1 -1
  170. package/dist/tools/public-filter.d.ts.map +1 -1
  171. package/dist/tools/public-filter.js +6 -0
  172. package/dist/tools/public-filter.js.map +1 -1
  173. package/dist/tools/publishers.d.ts.map +1 -1
  174. package/dist/tools/publishers.js +6 -0
  175. package/dist/tools/publishers.js.map +1 -1
  176. package/dist/tools/recordings.d.ts.map +1 -1
  177. package/dist/tools/recordings.js +15 -0
  178. package/dist/tools/recordings.js.map +1 -1
  179. package/dist/tools/recovery-hints.d.ts.map +1 -1
  180. package/dist/tools/recovery-hints.js +105 -0
  181. package/dist/tools/recovery-hints.js.map +1 -1
  182. package/dist/tools/release-rich.d.ts.map +1 -1
  183. package/dist/tools/release-rich.js +4 -2
  184. package/dist/tools/release-rich.js.map +1 -1
  185. package/dist/tools/releases.d.ts.map +1 -1
  186. package/dist/tools/releases.js +55 -0
  187. package/dist/tools/releases.js.map +1 -1
  188. package/dist/tools/report-issue.d.ts.map +1 -1
  189. package/dist/tools/report-issue.js +3 -0
  190. package/dist/tools/report-issue.js.map +1 -1
  191. package/dist/tools/royalties.d.ts.map +1 -1
  192. package/dist/tools/royalties.js +18 -3
  193. package/dist/tools/royalties.js.map +1 -1
  194. package/dist/tools/search.d.ts.map +1 -1
  195. package/dist/tools/search.js +10 -1
  196. package/dist/tools/search.js.map +1 -1
  197. package/dist/tools/send.d.ts.map +1 -1
  198. package/dist/tools/send.js +9 -0
  199. package/dist/tools/send.js.map +1 -1
  200. package/dist/tools/sessions.d.ts.map +1 -1
  201. package/dist/tools/sessions.js +12 -0
  202. package/dist/tools/sessions.js.map +1 -1
  203. package/dist/tools/settings.d.ts.map +1 -1
  204. package/dist/tools/settings.js +30 -3
  205. package/dist/tools/settings.js.map +1 -1
  206. package/dist/tools/share-links.d.ts.map +1 -1
  207. package/dist/tools/share-links.js +15 -0
  208. package/dist/tools/share-links.js.map +1 -1
  209. package/dist/tools/share-send.d.ts +28 -0
  210. package/dist/tools/share-send.d.ts.map +1 -0
  211. package/dist/tools/share-send.js +131 -0
  212. package/dist/tools/share-send.js.map +1 -0
  213. package/dist/tools/sharing.d.ts +29 -0
  214. package/dist/tools/sharing.d.ts.map +1 -0
  215. package/dist/tools/sharing.js +131 -0
  216. package/dist/tools/sharing.js.map +1 -0
  217. package/dist/tools/signup.d.ts.map +1 -1
  218. package/dist/tools/signup.js +3 -0
  219. package/dist/tools/signup.js.map +1 -1
  220. package/dist/tools/skills.d.ts +25 -0
  221. package/dist/tools/skills.d.ts.map +1 -0
  222. package/dist/tools/skills.js +144 -0
  223. package/dist/tools/skills.js.map +1 -0
  224. package/dist/tools/split-sheets.d.ts.map +1 -1
  225. package/dist/tools/split-sheets.js +22 -1
  226. package/dist/tools/split-sheets.js.map +1 -1
  227. package/dist/tools/storage-config.d.ts.map +1 -1
  228. package/dist/tools/storage-config.js +6 -0
  229. package/dist/tools/storage-config.js.map +1 -1
  230. package/dist/tools/subscription.d.ts.map +1 -1
  231. package/dist/tools/subscription.js +9 -10
  232. package/dist/tools/subscription.js.map +1 -1
  233. package/dist/tools/sync-placements.d.ts.map +1 -1
  234. package/dist/tools/sync-placements.js +20 -2
  235. package/dist/tools/sync-placements.js.map +1 -1
  236. package/dist/tools/team.d.ts.map +1 -1
  237. package/dist/tools/team.js +15 -0
  238. package/dist/tools/team.js.map +1 -1
  239. package/dist/tools/telegram.d.ts.map +1 -1
  240. package/dist/tools/telegram.js +9 -0
  241. package/dist/tools/telegram.js.map +1 -1
  242. package/dist/tools/uploads.d.ts.map +1 -1
  243. package/dist/tools/uploads.js +6 -0
  244. package/dist/tools/uploads.js.map +1 -1
  245. package/dist/tools/works.d.ts +4 -0
  246. package/dist/tools/works.d.ts.map +1 -1
  247. package/dist/tools/works.js +83 -3
  248. package/dist/tools/works.js.map +1 -1
  249. package/package.json +7 -6
  250. package/scripts/build-skills.ts +229 -0
  251. package/server.json +2 -2
@@ -2,6 +2,7 @@
2
2
  import { WorksTools } from "./works.js";
3
3
  import { PeopleTools } from "./people.js";
4
4
  import { GroupsTools } from "./groups.js";
5
+ import { ShareSendTools } from "./share-send.js";
5
6
  import { RecordingsTools } from "./recordings.js";
6
7
  import { SearchTools } from "./search.js";
7
8
  import { LicensingTools } from "./licensing.js";
@@ -10,6 +11,7 @@ import { AudioFilesTools } from "./audio-files.js";
10
11
  import { MultimediaTools } from "./multimedia.js";
11
12
  import { AgreementsTools } from "./agreements.js";
12
13
  import { MemoryTools } from "./memory.js";
14
+ import { SkillsTools } from "./skills.js";
13
15
  import { EnrichmentTools } from "./enrichment.js";
14
16
  import { BulkTools } from "./bulk.js";
15
17
  import { ExportTools } from "./exports.js";
@@ -44,9 +46,13 @@ import { SubscriptionTools } from "./subscription.js";
44
46
  import { OnboardingTools } from "./onboarding.js";
45
47
  import { ReportIssueTools } from "./report-issue.js";
46
48
  import { MyReportedIssuesTools } from "./my-reported-issues.js";
49
+ import { MyRecentQuestionsTools } from "./my-recent-questions.js";
47
50
  import { AgentIdentityTools } from "./agent-identity.js";
48
51
  import { RoyaltiesTools } from "./royalties.js";
49
52
  import { ShareLinksTools } from "./share-links.js";
53
+ import { SharingTools } from "./sharing.js";
54
+ import { ExplainabilityTools } from "./explainability.js";
55
+ import { AccessSimulateTools } from "./access-simulate.js";
50
56
  import { DisputesTools } from "./disputes.js";
51
57
  import { CustodyTools } from "./custody.js";
52
58
  import { PurchasesTools } from "./purchases.js";
@@ -66,6 +72,46 @@ import { getToolMetadata } from "./metadata.js";
66
72
  import { getRecoveryHint } from "./recovery-hints.js";
67
73
  import { generateConfirmationToken, validateAndConsumeToken, } from "@withpica/mcp-utils";
68
74
  import { buildSessionState, incrementSessionMutations, resetSinceLastBriefing, } from "@withpica/mcp-utils";
75
+ /**
76
+ * ADR-230 — derive the MCP-standard 3-value risk classification from the
77
+ * declared tier. Sourced once at registration and at audit-write time so
78
+ * the contract that hints, audit row, and confirmation requirement all
79
+ * agree is mechanical.
80
+ */
81
+ export function tierToRiskLevel(tier) {
82
+ switch (tier) {
83
+ case "read":
84
+ return "safe";
85
+ case "draft":
86
+ case "write":
87
+ return "mutating";
88
+ case "destructive":
89
+ return "destructive";
90
+ }
91
+ }
92
+ /**
93
+ * ADR-230 — derive the required API-key scope from the declared tier.
94
+ *
95
+ * - `read` → `read:<resource>` (write:* satisfies via `hasScope`).
96
+ * - `draft|write` → `write:<resource>`.
97
+ * - `destructive` → `destructive:*` (admin satisfies via `hasScope`); the
98
+ * write:<resource> scope is also required for the
99
+ * underlying write surface.
100
+ *
101
+ * Returns the *additional* scope tier beyond per-resource. Resource scope
102
+ * is composed at call-site by `lib/services/mcp-scopes.ts`.
103
+ */
104
+ export function tierToScopeKind(tier) {
105
+ switch (tier) {
106
+ case "read":
107
+ return "read";
108
+ case "draft":
109
+ case "write":
110
+ return "write";
111
+ case "destructive":
112
+ return "destructive";
113
+ }
114
+ }
69
115
  /**
70
116
  * Build a tool description with metadata injected.
71
117
  * Format: "[category] original description"
@@ -85,6 +131,64 @@ export const CATEGORY_DISPLAY = {
85
131
  comms: "send and share",
86
132
  settings: "manage your account and team",
87
133
  };
134
+ /**
135
+ * Sanitize tool parameters for `mcp_audit_log.parameters`. Pure function
136
+ * so it's directly testable; the registry's private method delegates here.
137
+ *
138
+ * Security audit 2026-05-11 P2. The audit log is queryable by anyone with
139
+ * team-portal access. Tools like `pica_sign_in`, `pica_share_links_*`,
140
+ * and `team_comms_send` previously landed bare emails / share tokens /
141
+ * OAuth refresh tokens in the log because the only redaction was a
142
+ * `delete sanitized.confirmation_token` + length truncation.
143
+ *
144
+ * Deny-list is keyed off the param name (case-insensitive substring
145
+ * match) — covers `password`, `api_key`, `token`, `secret`,
146
+ * `authorization`, plus `confirmation_token` via the `token` substring.
147
+ * Sister keys like `refresh_token` / `id_token` / `access_token` /
148
+ * `share_token` / `client_secret` also match.
149
+ *
150
+ * Email values (regardless of param key) get the local-part masked to
151
+ * the first two characters: `jane@example.com` → `ja***@example.com`.
152
+ * Locals ≤ 2 chars become all-asterisk.
153
+ *
154
+ * Strings > 500 chars are truncated to 500 + `...[truncated]`.
155
+ *
156
+ * Exported under `_internal` so test files can exercise the function
157
+ * without instantiating a `ToolRegistry`.
158
+ */
159
+ function sanitizeAuditParams(args) {
160
+ const sanitized = { ...args };
161
+ const denyKeySubstrings = [
162
+ "password",
163
+ "api_key",
164
+ "token",
165
+ "secret",
166
+ "authorization",
167
+ ];
168
+ for (const [key, value] of Object.entries(sanitized)) {
169
+ const lowerKey = key.toLowerCase();
170
+ if (denyKeySubstrings.some((needle) => lowerKey.includes(needle))) {
171
+ sanitized[key] = "[redacted]";
172
+ continue;
173
+ }
174
+ if (typeof value === "string" &&
175
+ /^[^\s@]{1,64}@[^\s@]{1,255}\.[^\s@]+$/.test(value)) {
176
+ const atIdx = value.indexOf("@");
177
+ const local = value.slice(0, atIdx);
178
+ const domain = value.slice(atIdx);
179
+ const maskedLocal = local.length <= 2
180
+ ? "*".repeat(local.length)
181
+ : `${local.slice(0, 2)}***`;
182
+ sanitized[key] = `${maskedLocal}${domain}`;
183
+ continue;
184
+ }
185
+ if (typeof value === "string" && value.length > 500) {
186
+ sanitized[key] = value.substring(0, 500) + "...[truncated]";
187
+ }
188
+ }
189
+ return sanitized;
190
+ }
191
+ export const _internal = { sanitizeAuditParams };
88
192
  export class ToolRegistry {
89
193
  tools;
90
194
  pica;
@@ -111,6 +215,25 @@ export class ToolRegistry {
111
215
  setCallerContext(context) {
112
216
  this.callerContext = context;
113
217
  }
218
+ /**
219
+ * Read clientInfo from the MCP `initialize` handshake. SDK populates this
220
+ * on the per-request `Server` for HTTP and on the long-lived `Server` for
221
+ * stdio; either way getClientVersion() returns the same shape after the
222
+ * handshake completes. Returns empty when ctx is absent or the handshake
223
+ * hasn't run (lobby-mode dispatches that bypass the per-request transport).
224
+ *
225
+ * Stamped onto audit rows as provenance only — never used as a permission
226
+ * boundary, since clientInfo is self-declared by the client.
227
+ */
228
+ extractClientInfo(ctx) {
229
+ const info = ctx?.server?.getClientVersion?.();
230
+ if (!info)
231
+ return {};
232
+ return {
233
+ client_name: typeof info.name === "string" ? info.name : undefined,
234
+ client_version: typeof info.version === "string" ? info.version : undefined,
235
+ };
236
+ }
114
237
  /**
115
238
  * Register all available tools
116
239
  */
@@ -159,6 +282,11 @@ export class ToolRegistry {
159
282
  groupsTools.getTools().forEach((tool) => {
160
283
  this.tools.set(tool.definition.name, tool);
161
284
  });
285
+ // Share-send tool (ADR-231)
286
+ const shareSendTools = new ShareSendTools(pica);
287
+ shareSendTools.getTools().forEach((tool) => {
288
+ this.tools.set(tool.definition.name, tool);
289
+ });
162
290
  // Recordings tools
163
291
  const recordingsTools = new RecordingsTools(pica);
164
292
  recordingsTools.getTools().forEach((tool) => {
@@ -199,6 +327,11 @@ export class ToolRegistry {
199
327
  memoryTools.getTools().forEach((tool) => {
200
328
  this.tools.set(tool.definition.name, tool);
201
329
  });
330
+ // Skills tools (ADR-140 Phase 2b — SEP-2640 bridge tool lane)
331
+ const skillsTools = new SkillsTools();
332
+ skillsTools.getTools().forEach((tool) => {
333
+ this.tools.set(tool.definition.name, tool);
334
+ });
202
335
  // Enrichment tools
203
336
  const enrichmentTools = new EnrichmentTools(pica);
204
337
  enrichmentTools.getTools().forEach((tool) => {
@@ -377,6 +510,21 @@ export class ToolRegistry {
377
510
  myReportedIssuesTools.getTools().forEach((tool) => {
378
511
  this.tools.set(tool.definition.name, tool);
379
512
  });
513
+ // My-recent-questions tool (ADR-226 Phase 5 — pica_my_recent_questions)
514
+ // — user-scoped read of the Phase 4 substrate (assistant_interactions
515
+ // joined with mcp_sessions for client identity). Atlas-resolved entry
516
+ // for "what did I ask the AI?".
517
+ //
518
+ // ADR-230 post-ship cleanup — the tier resolver is the registry's
519
+ // own `getToolTier` shape (declared `tier` on each tool definition).
520
+ // Lint Rule 13 makes tier required for every customer MCP tool, so
521
+ // this resolver returns a populated value for all known tools and
522
+ // `undefined` only for unknown / meta tools — `dominantTier` falls
523
+ // back to "read" in that case.
524
+ const myRecentQuestionsTools = new MyRecentQuestionsTools(pica, (name) => this.tools.get(name)?.definition.tier);
525
+ myRecentQuestionsTools.getTools().forEach((tool) => {
526
+ this.tools.set(tool.definition.name, tool);
527
+ });
380
528
  // Agent identity tools (ADR-185 Part 1 — session-auth-only MCP surface).
381
529
  // The three tools refuse grant-auth callers at the HTTP API layer;
382
530
  // the stdio path carries an API key so behaviour is identical either
@@ -395,6 +543,21 @@ export class ToolRegistry {
395
543
  shareLinksTools.getTools().forEach((tool) => {
396
544
  this.tools.set(tool.definition.name, tool);
397
545
  });
546
+ // Sharing trace tool (ADR-232 § Decision 2)
547
+ const sharingTools = new SharingTools(pica);
548
+ sharingTools.getTools().forEach((tool) => {
549
+ this.tools.set(tool.definition.name, tool);
550
+ });
551
+ // Explainability tools (ADR-232 § Decision 3)
552
+ const explainabilityTools = new ExplainabilityTools(pica);
553
+ explainabilityTools.getTools().forEach((tool) => {
554
+ this.tools.set(tool.definition.name, tool);
555
+ });
556
+ // Access simulator — read-only "who would see what?" preview.
557
+ const accessSimulateTools = new AccessSimulateTools(pica);
558
+ accessSimulateTools.getTools().forEach((tool) => {
559
+ this.tools.set(tool.definition.name, tool);
560
+ });
398
561
  // Disputes tools (ADR-139 Step 3)
399
562
  const disputesTools = new DisputesTools(pica);
400
563
  disputesTools.getTools().forEach((tool) => {
@@ -456,72 +619,15 @@ export class ToolRegistry {
456
619
  });
457
620
  }
458
621
  }
459
- // ── Write-safety classification ──
460
- // Destructive tools require confirmation before AND summary after.
461
- // Mutating tools should present what they'll do and confirm after.
462
- // Safe (read-only) tools need no confirmation.
463
- static DESTRUCTIVE_PATTERNS = [
464
- "_delete",
465
- "_bulk_delete",
466
- "_merge",
467
- "_remove",
468
- "_disconnect",
469
- ];
470
- static MUTATING_PATTERNS = [
471
- "_create",
472
- "_update",
473
- "_bulk_update",
474
- "_invite",
475
- "_link",
476
- "_send_",
477
- "_import_",
478
- "_execute",
479
- "_ingest",
480
- "_enrich",
481
- "_verify",
482
- "_mark_",
483
- "_review",
484
- "_purchase",
485
- "_submit",
486
- "_generate",
487
- "_set_default",
488
- "_duplicate",
489
- "_toggle",
490
- "_save",
491
- "_upload",
492
- "_complete",
493
- "_analyze",
494
- "_analyse",
495
- "_identify",
496
- "_resend",
497
- "_notify",
498
- ];
499
- // Read-only tools that match mutating patterns by name but are actually safe
500
- static SAFE_OVERRIDES = new Set([
501
- "pica_collaborators_invites_list",
502
- "pica_enrichment_candidates",
503
- "pica_enrichment_compare",
504
- "pica_find_duplicates",
505
- "pica_import_analyze",
506
- "pica_import_validate",
507
- "pica_import_fields",
508
- "pica_import_template",
509
- "pica_import_documents_query",
510
- "pica_import_documents_inspect",
511
- "pica_send_query",
512
- "pica_share_links_list",
513
- ]);
514
- classifyTool(name) {
515
- if (ToolRegistry.SAFE_OVERRIDES.has(name)) {
516
- return "safe";
517
- }
518
- if (ToolRegistry.DESTRUCTIVE_PATTERNS.some((p) => name.includes(p))) {
519
- return "destructive";
520
- }
521
- if (ToolRegistry.MUTATING_PATTERNS.some((p) => name.includes(p))) {
522
- return "mutating";
523
- }
524
- return "safe";
622
+ /**
623
+ * ADR-230 declared tier lookup by tool name. Used by the HTTP MCP
624
+ * dispatcher (`app/api/mcp/route.ts`) to enforce the elevated
625
+ * `destructive:*` scope on top of the resource-level scope from
626
+ * `lib/services/mcp-scopes`. Returns undefined for unknown tools or
627
+ * tools that haven't declared tier yet (legacy tolerance).
628
+ */
629
+ getToolTier(name) {
630
+ return this.tools.get(name)?.definition.tier;
525
631
  }
526
632
  /**
527
633
  * List all available tools with write-safety prefixes injected.
@@ -543,9 +649,14 @@ export class ToolRegistry {
543
649
  return toolsToList.map((tool) => {
544
650
  let definition = tool.definition;
545
651
  const metadata = getToolMetadata(definition.name);
546
- // Inject confirmation_token into destructive tool schemas
547
- const isDestructive = metadata?.risk === "destructive" ||
548
- (!metadata && this.classifyTool(definition.name) === "destructive");
652
+ // ADR-230 declared tier is the source of truth for hints + risk.
653
+ // Lint Rule 13 enforces tier presence on every customer MCP tool;
654
+ // metadata.risk is a quiet fallback for the unlikely case of a tool
655
+ // that bypasses lint.
656
+ const declaredTier = definition.tier;
657
+ const isDestructive = declaredTier
658
+ ? declaredTier === "destructive"
659
+ : metadata?.risk === "destructive";
549
660
  if (isDestructive) {
550
661
  definition = {
551
662
  ...definition,
@@ -573,6 +684,26 @@ export class ToolRegistry {
573
684
  },
574
685
  };
575
686
  }
687
+ // ADR-230 — derive hints from declared tier when present (every
688
+ // tool registered in mcp-server/src/tools/*.ts has tier as of
689
+ // PR-1; the metadata branch stays as a defensive code path).
690
+ if (declaredTier && metadata) {
691
+ return {
692
+ ...definition,
693
+ description: injectMetadataIntoDescription(tool.definition.description, metadata),
694
+ annotations: {
695
+ title: metadata.display_name,
696
+ readOnlyHint: declaredTier === "read",
697
+ destructiveHint: declaredTier === "destructive",
698
+ idempotentHint: declaredTier !== "destructive" && metadata.retry_safe,
699
+ openWorldHint: false,
700
+ // ADR-199 extended annotation classes — read by ChatGPT/Claude.ai
701
+ // connectors directly. Derived from declared tier so the four
702
+ // surfaces (hint, riskLevel, audit row, scope) all agree.
703
+ ...this.deriveExtendedAnnotations(definition, metadata, undefined, declaredTier),
704
+ },
705
+ };
706
+ }
576
707
  if (metadata) {
577
708
  return {
578
709
  ...definition,
@@ -583,25 +714,25 @@ export class ToolRegistry {
583
714
  destructiveHint: metadata.risk === "destructive",
584
715
  idempotentHint: metadata.risk !== "destructive" && metadata.retry_safe,
585
716
  openWorldHint: false,
586
- // ADR-199 extended annotation classes — read by ChatGPT/Claude.ai
587
- // connectors directly. Derived from existing metadata so existing
588
- // tools get this for free without per-tool edits.
589
717
  ...this.deriveExtendedAnnotations(definition, metadata),
590
718
  },
591
719
  };
592
720
  }
593
- // Fallback to old pattern-matching for any tool without explicit metadata
594
- const classification = this.classifyTool(definition.name);
721
+ // ADR-230 tools without a TOOL_METADATA entry (the 5 lobby
722
+ // meta-tools sign_in/out/discover/tool_details/execute) still
723
+ // declare tier. Source hints from declared tier; default to read
724
+ // when tier is somehow missing (lint blocks; defensive only).
725
+ const tierForHints = declaredTier ?? "read";
595
726
  const annotations = {
596
727
  title: definition.name,
597
- readOnlyHint: classification === "safe",
598
- destructiveHint: classification === "destructive",
728
+ readOnlyHint: tierForHints === "read",
729
+ destructiveHint: tierForHints === "destructive",
599
730
  idempotentHint: false,
600
731
  openWorldHint: false,
601
- // ADR-199: derive the extended classes even without explicit
602
- // metadata so the fallback tools also surface category/risk to
603
- // ChatGPT/Claude.ai connectors.
604
- ...this.deriveExtendedAnnotations(definition, null, classification),
732
+ // ADR-199: derive the extended classes from declared tier so the
733
+ // category/risk surface stays aligned with the rest of the
734
+ // hint/audit/scope chain.
735
+ ...this.deriveExtendedAnnotations(definition, null, undefined, tierForHints),
605
736
  };
606
737
  // No risk prefix needed — annotations handle this structurally
607
738
  return { ...definition, annotations };
@@ -616,9 +747,16 @@ export class ToolRegistry {
616
747
  * edits required for the retrofit. Future tool authors can override by
617
748
  * setting fields directly on definition.annotations.
618
749
  */
619
- deriveExtendedAnnotations(definition, metadata, classification) {
750
+ deriveExtendedAnnotations(definition, metadata, _classification, declaredTier) {
620
751
  const name = definition.name;
621
- const risk = metadata?.risk ?? classification ?? "safe";
752
+ // ADR-230 declared tier is the source of truth. metadata.risk
753
+ // remains a quiet fallback for any tool that bypasses lint Rule 13.
754
+ // (The `_classification` parameter is retained as an unused
755
+ // positional slot so legacy call signatures continue to compile;
756
+ // its value is now ignored — classifyTool was removed in PR-3.)
757
+ const risk = declaredTier
758
+ ? tierToRiskLevel(declaredTier)
759
+ : (metadata?.risk ?? "safe");
622
760
  // Side-effecting external systems: telegram, send-hub, comms, broadcasts.
623
761
  // Pattern-derived so new tools inherit without edits.
624
762
  const externalSideEffectPatterns = [
@@ -638,10 +776,16 @@ export class ToolRegistry {
638
776
  // the agent wants to confirm — the gap report flagged this as a useful
639
777
  // class for ChatGPT to surface in the UI.
640
778
  const requiresFollowUpInspection = risk === "mutating" || risk === "destructive";
779
+ // ADR-230 — destructive tier always requires confirmation. Other tiers
780
+ // never do via this annotation (per-tool opt-in for `write` tier is
781
+ // expressed through `previewMode: "two_step_token"`, not here).
782
+ const requiresConfirmation = declaredTier
783
+ ? declaredTier === "destructive"
784
+ : risk === "destructive";
641
785
  return {
642
786
  riskLevel: risk,
643
787
  category: metadata?.category,
644
- requiresConfirmation: risk === "destructive",
788
+ requiresConfirmation,
645
789
  createsExternalSideEffects,
646
790
  returnsPaginated,
647
791
  requiresFollowUpInspection,
@@ -677,6 +821,15 @@ export class ToolRegistry {
677
821
  display_name: displayName,
678
822
  };
679
823
  }
824
+ case "pica_works_bulk_move_to_performances": {
825
+ const count = args.ids?.length || 0;
826
+ return {
827
+ action: "bulk reclassify works → performances",
828
+ target: `${count} ${count === 1 ? "work" : "works"}`,
829
+ warning: `this creates ${count} new live_performance row(s) and removes the source work(s) from the catalog. for works with verified identifiers (ISRC/ISWC/MLC) the source is soft-deleted — credits and agreement links survive but are orphaned to the removed work; for unverified works the deletion is permanent. performance metadata is sparse by default and will need filling in.`,
830
+ display_name: displayName,
831
+ };
832
+ }
680
833
  case "pica_people_delete": {
681
834
  const person = await pica.people.get(args.id).catch(() => null);
682
835
  const personName = person?.first_name
@@ -689,6 +842,15 @@ export class ToolRegistry {
689
842
  display_name: displayName,
690
843
  };
691
844
  }
845
+ case "pica_people_bulk_delete": {
846
+ const count = args.ids?.length || 0;
847
+ return {
848
+ action: "bulk delete people",
849
+ target: `${count} ${count === 1 ? "person" : "people"}`,
850
+ warning: `this will soft-delete ${count} person record(s). credits and agreements remain linked to the removed records.`,
851
+ display_name: displayName,
852
+ };
853
+ }
692
854
  case "pica_agreements_delete": {
693
855
  const agreementResult = await pica.agreements
694
856
  .get(args.id)
@@ -834,18 +996,11 @@ export class ToolRegistry {
834
996
  }
835
997
  }
836
998
  /**
837
- * Sanitize tool parameters for audit logging.
838
- * Strips confirmation tokens and truncates large string values.
999
+ * Sanitize tool parameters for audit logging — delegates to the
1000
+ * pure-function impl below so it can be exercised directly in tests.
839
1001
  */
840
1002
  sanitizeParams(args) {
841
- const sanitized = { ...args };
842
- delete sanitized.confirmation_token;
843
- for (const [key, value] of Object.entries(sanitized)) {
844
- if (typeof value === "string" && value.length > 500) {
845
- sanitized[key] = value.substring(0, 500) + "...[truncated]";
846
- }
847
- }
848
- return sanitized;
1003
+ return sanitizeAuditParams(args);
849
1004
  }
850
1005
  /**
851
1006
  * ADR-208 Primitive B — opaque per-org cache key.
@@ -1018,10 +1173,22 @@ export class ToolRegistry {
1018
1173
  throw new ToolExecutionError(`Tool not found: ${name}`);
1019
1174
  }
1020
1175
  const metadata = getToolMetadata(name);
1176
+ // ADR-230 — declared tier is the source of truth for the freshness +
1177
+ // confirmation gates and the audit-log surfaces. Fall back to
1178
+ // metadata.risk only if the tool somehow lacks tier (lint blocks
1179
+ // missing tier on creator + team + directory MCPs as of PR-2).
1180
+ const declaredTier = tool.definition.tier;
1181
+ const fallbackRisk = metadata?.risk ?? "safe";
1182
+ const auditTier = declaredTier ??
1183
+ (fallbackRisk === "safe"
1184
+ ? "read"
1185
+ : fallbackRisk === "destructive"
1186
+ ? "destructive"
1187
+ : "write");
1188
+ const auditRiskLevel = tierToRiskLevel(auditTier);
1189
+ const isDestructive = auditTier === "destructive";
1021
1190
  // ── Session freshness gate (ADR-151) ──
1022
- if (metadata?.risk === "destructive" &&
1023
- this.config &&
1024
- !this.config.lobbyMode) {
1191
+ if (isDestructive && this.config && !this.config.lobbyMode) {
1025
1192
  const creds = readCredentials(this.config.credentialsPath);
1026
1193
  if (creds?.authenticated_at) {
1027
1194
  const authAge = Date.now() - new Date(creds.authenticated_at).getTime();
@@ -1039,12 +1206,15 @@ export class ToolRegistry {
1039
1206
  }
1040
1207
  }
1041
1208
  }
1042
- // ── Destructive confirmation gate ──
1043
- if (metadata?.risk === "destructive") {
1209
+ // ── Destructive confirmation gate (ADR-230 — keyed on declared tier) ──
1210
+ if (isDestructive) {
1044
1211
  const confirmationToken = args.confirmation_token;
1045
1212
  if (!confirmationToken) {
1046
- // First call — generate preview and return confirmation challenge
1047
- const token = generateConfirmationToken(name, args);
1213
+ // First call — generate preview and return confirmation challenge.
1214
+ // ADR-230 PR-4 — token state lives in Postgres for HTTP MCP under
1215
+ // Fluid Compute (or in-memory for stdio); both paths via the
1216
+ // active ConfirmationStore singleton, so callers stay unchanged.
1217
+ const token = await generateConfirmationToken(name, args);
1048
1218
  const preview = await this.buildDestructivePreview(name, args);
1049
1219
  const response = {
1050
1220
  content: [
@@ -1070,6 +1240,7 @@ export class ToolRegistry {
1070
1240
  ?.logToolExecution({
1071
1241
  tool_name: name,
1072
1242
  tool_category: metadata?.category || "unknown",
1243
+ tier: "destructive",
1073
1244
  risk_level: "destructive",
1074
1245
  parameters: this.sanitizeParams(args),
1075
1246
  result_status: "confirmation_required",
@@ -1077,12 +1248,14 @@ export class ToolRegistry {
1077
1248
  execution_time_ms: Date.now() - startTime,
1078
1249
  caller_identity: this.callerContext.callerIdentity,
1079
1250
  transport: this.callerContext.transport,
1251
+ ...this.extractClientInfo(ctx),
1080
1252
  })
1081
1253
  .catch(() => { }); // Never block on audit
1082
1254
  return response;
1083
1255
  }
1084
- // Second call — validate token
1085
- const validation = validateAndConsumeToken(confirmationToken, name, args);
1256
+ // Second call — validate token (Postgres-backed under Fluid Compute
1257
+ // since ADR-230 PR-4; the active store handles cross-instance state).
1258
+ const validation = await validateAndConsumeToken(confirmationToken, name, args);
1086
1259
  if (!validation.valid) {
1087
1260
  return {
1088
1261
  content: [
@@ -1115,10 +1288,10 @@ export class ToolRegistry {
1115
1288
  }
1116
1289
  }
1117
1290
  // ADR-208 Primitive B — ambient session-state on write-tool results.
1118
- // Read tools (`risk: safe` ⇒ `readOnlyHint: true`) skip this entirely so
1119
- // the high-volume read path stays cheap. `result.isError === true` also
1120
- // skips because a failed mutation didn't change catalog state.
1121
- const isWriteTool = (metadata?.risk ?? this.classifyTool(name)) !== "safe";
1291
+ // Read tools (`tier: "read"`) skip this entirely so the high-volume
1292
+ // read path stays cheap. `result.isError === true` also skips because
1293
+ // a failed mutation didn't change catalog state.
1294
+ const isWriteTool = auditRiskLevel !== "safe";
1122
1295
  if (isWriteTool && result && !result.isError) {
1123
1296
  await this.attachSessionState(result, name);
1124
1297
  }
@@ -1144,12 +1317,14 @@ export class ToolRegistry {
1144
1317
  ?.logToolExecution({
1145
1318
  tool_name: name,
1146
1319
  tool_category: metadata?.category || "unknown",
1147
- risk_level: metadata?.risk || this.classifyTool(name),
1320
+ tier: auditTier,
1321
+ risk_level: auditRiskLevel,
1148
1322
  parameters: this.sanitizeParams(args),
1149
1323
  result_status: "success",
1150
1324
  execution_time_ms: Date.now() - startTime,
1151
1325
  caller_identity: this.callerContext.callerIdentity,
1152
1326
  transport: this.callerContext.transport,
1327
+ ...this.extractClientInfo(ctx),
1153
1328
  })
1154
1329
  .catch(() => { }); // Never block on audit
1155
1330
  return result;
@@ -1170,13 +1345,19 @@ export class ToolRegistry {
1170
1345
  ?.logToolExecution({
1171
1346
  tool_name: name,
1172
1347
  tool_category: metadata?.category || "unknown",
1173
- risk_level: metadata?.risk || this.classifyTool(name),
1348
+ tier: auditTier,
1349
+ risk_level: auditRiskLevel,
1174
1350
  parameters: this.sanitizeParams(args),
1175
1351
  result_status: "error",
1176
1352
  error_code: parsed.error,
1353
+ // Stage A — forward the parsed body's message so mcp_audit_log
1354
+ // captures the underlying cause. Without this every error row had
1355
+ // error_message=null, leaving UNKNOWN_ERROR rows opaque.
1356
+ error_message: typeof parsed.message === "string" ? parsed.message : undefined,
1177
1357
  execution_time_ms: Date.now() - startTime,
1178
1358
  caller_identity: this.callerContext.callerIdentity,
1179
1359
  transport: this.callerContext.transport,
1360
+ ...this.extractClientInfo(ctx),
1180
1361
  })
1181
1362
  .catch(() => { }); // Never block on audit
1182
1363
  return {