@withpica/mcp-server 2.52.0 → 2.52.2

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 +93 -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 -2
  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 +5 -3
  15. package/dist/server-instructions.d.ts.map +1 -1
  16. package/dist/server-instructions.js +9 -5
  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 -3
  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 +65 -18
  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 -114
  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 -6
  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 -1
  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 +8 -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";
@@ -36,7 +38,6 @@ import { CalendarTools } from "./calendar.js";
36
38
  import { SplitSheetsTools } from "./split-sheets.js";
37
39
  import { AgreementTypesTools } from "./agreement-types.js";
38
40
  import { DashboardTools } from "./dashboard.js";
39
- import { IntegrationsTools } from "./integrations.js";
40
41
  import { SettingsTools } from "./settings.js";
41
42
  import { FeedbackTools } from "./feedback.js";
42
43
  import { StorageConfigTools } from "./storage-config.js";
@@ -44,9 +45,13 @@ import { SubscriptionTools } from "./subscription.js";
44
45
  import { OnboardingTools } from "./onboarding.js";
45
46
  import { ReportIssueTools } from "./report-issue.js";
46
47
  import { MyReportedIssuesTools } from "./my-reported-issues.js";
48
+ import { MyRecentQuestionsTools } from "./my-recent-questions.js";
47
49
  import { AgentIdentityTools } from "./agent-identity.js";
48
50
  import { RoyaltiesTools } from "./royalties.js";
49
51
  import { ShareLinksTools } from "./share-links.js";
52
+ import { SharingTools } from "./sharing.js";
53
+ import { ExplainabilityTools } from "./explainability.js";
54
+ import { AccessSimulateTools } from "./access-simulate.js";
50
55
  import { DisputesTools } from "./disputes.js";
51
56
  import { CustodyTools } from "./custody.js";
52
57
  import { PurchasesTools } from "./purchases.js";
@@ -66,6 +71,46 @@ import { getToolMetadata } from "./metadata.js";
66
71
  import { getRecoveryHint } from "./recovery-hints.js";
67
72
  import { generateConfirmationToken, validateAndConsumeToken, } from "@withpica/mcp-utils";
68
73
  import { buildSessionState, incrementSessionMutations, resetSinceLastBriefing, } from "@withpica/mcp-utils";
74
+ /**
75
+ * ADR-230 — derive the MCP-standard 3-value risk classification from the
76
+ * declared tier. Sourced once at registration and at audit-write time so
77
+ * the contract that hints, audit row, and confirmation requirement all
78
+ * agree is mechanical.
79
+ */
80
+ export function tierToRiskLevel(tier) {
81
+ switch (tier) {
82
+ case "read":
83
+ return "safe";
84
+ case "draft":
85
+ case "write":
86
+ return "mutating";
87
+ case "destructive":
88
+ return "destructive";
89
+ }
90
+ }
91
+ /**
92
+ * ADR-230 — derive the required API-key scope from the declared tier.
93
+ *
94
+ * - `read` → `read:<resource>` (write:* satisfies via `hasScope`).
95
+ * - `draft|write` → `write:<resource>`.
96
+ * - `destructive` → `destructive:*` (admin satisfies via `hasScope`); the
97
+ * write:<resource> scope is also required for the
98
+ * underlying write surface.
99
+ *
100
+ * Returns the *additional* scope tier beyond per-resource. Resource scope
101
+ * is composed at call-site by `lib/services/mcp-scopes.ts`.
102
+ */
103
+ export function tierToScopeKind(tier) {
104
+ switch (tier) {
105
+ case "read":
106
+ return "read";
107
+ case "draft":
108
+ case "write":
109
+ return "write";
110
+ case "destructive":
111
+ return "destructive";
112
+ }
113
+ }
69
114
  /**
70
115
  * Build a tool description with metadata injected.
71
116
  * Format: "[category] original description"
@@ -85,6 +130,64 @@ export const CATEGORY_DISPLAY = {
85
130
  comms: "send and share",
86
131
  settings: "manage your account and team",
87
132
  };
133
+ /**
134
+ * Sanitize tool parameters for `mcp_audit_log.parameters`. Pure function
135
+ * so it's directly testable; the registry's private method delegates here.
136
+ *
137
+ * Security audit 2026-05-11 P2. The audit log is queryable by anyone with
138
+ * team-portal access. Tools like `pica_sign_in`, `pica_share_links_*`,
139
+ * and `team_comms_send` previously landed bare emails / share tokens /
140
+ * OAuth refresh tokens in the log because the only redaction was a
141
+ * `delete sanitized.confirmation_token` + length truncation.
142
+ *
143
+ * Deny-list is keyed off the param name (case-insensitive substring
144
+ * match) — covers `password`, `api_key`, `token`, `secret`,
145
+ * `authorization`, plus `confirmation_token` via the `token` substring.
146
+ * Sister keys like `refresh_token` / `id_token` / `access_token` /
147
+ * `share_token` / `client_secret` also match.
148
+ *
149
+ * Email values (regardless of param key) get the local-part masked to
150
+ * the first two characters: `jane@example.com` → `ja***@example.com`.
151
+ * Locals ≤ 2 chars become all-asterisk.
152
+ *
153
+ * Strings > 500 chars are truncated to 500 + `...[truncated]`.
154
+ *
155
+ * Exported under `_internal` so test files can exercise the function
156
+ * without instantiating a `ToolRegistry`.
157
+ */
158
+ function sanitizeAuditParams(args) {
159
+ const sanitized = { ...args };
160
+ const denyKeySubstrings = [
161
+ "password",
162
+ "api_key",
163
+ "token",
164
+ "secret",
165
+ "authorization",
166
+ ];
167
+ for (const [key, value] of Object.entries(sanitized)) {
168
+ const lowerKey = key.toLowerCase();
169
+ if (denyKeySubstrings.some((needle) => lowerKey.includes(needle))) {
170
+ sanitized[key] = "[redacted]";
171
+ continue;
172
+ }
173
+ if (typeof value === "string" &&
174
+ /^[^\s@]{1,64}@[^\s@]{1,255}\.[^\s@]+$/.test(value)) {
175
+ const atIdx = value.indexOf("@");
176
+ const local = value.slice(0, atIdx);
177
+ const domain = value.slice(atIdx);
178
+ const maskedLocal = local.length <= 2
179
+ ? "*".repeat(local.length)
180
+ : `${local.slice(0, 2)}***`;
181
+ sanitized[key] = `${maskedLocal}${domain}`;
182
+ continue;
183
+ }
184
+ if (typeof value === "string" && value.length > 500) {
185
+ sanitized[key] = value.substring(0, 500) + "...[truncated]";
186
+ }
187
+ }
188
+ return sanitized;
189
+ }
190
+ export const _internal = { sanitizeAuditParams };
88
191
  export class ToolRegistry {
89
192
  tools;
90
193
  pica;
@@ -111,6 +214,25 @@ export class ToolRegistry {
111
214
  setCallerContext(context) {
112
215
  this.callerContext = context;
113
216
  }
217
+ /**
218
+ * Read clientInfo from the MCP `initialize` handshake. SDK populates this
219
+ * on the per-request `Server` for HTTP and on the long-lived `Server` for
220
+ * stdio; either way getClientVersion() returns the same shape after the
221
+ * handshake completes. Returns empty when ctx is absent or the handshake
222
+ * hasn't run (lobby-mode dispatches that bypass the per-request transport).
223
+ *
224
+ * Stamped onto audit rows as provenance only — never used as a permission
225
+ * boundary, since clientInfo is self-declared by the client.
226
+ */
227
+ extractClientInfo(ctx) {
228
+ const info = ctx?.server?.getClientVersion?.();
229
+ if (!info)
230
+ return {};
231
+ return {
232
+ client_name: typeof info.name === "string" ? info.name : undefined,
233
+ client_version: typeof info.version === "string" ? info.version : undefined,
234
+ };
235
+ }
114
236
  /**
115
237
  * Register all available tools
116
238
  */
@@ -159,6 +281,11 @@ export class ToolRegistry {
159
281
  groupsTools.getTools().forEach((tool) => {
160
282
  this.tools.set(tool.definition.name, tool);
161
283
  });
284
+ // Share-send tool (ADR-231)
285
+ const shareSendTools = new ShareSendTools(pica);
286
+ shareSendTools.getTools().forEach((tool) => {
287
+ this.tools.set(tool.definition.name, tool);
288
+ });
162
289
  // Recordings tools
163
290
  const recordingsTools = new RecordingsTools(pica);
164
291
  recordingsTools.getTools().forEach((tool) => {
@@ -199,6 +326,11 @@ export class ToolRegistry {
199
326
  memoryTools.getTools().forEach((tool) => {
200
327
  this.tools.set(tool.definition.name, tool);
201
328
  });
329
+ // Skills tools (ADR-140 Phase 2b — SEP-2640 bridge tool lane)
330
+ const skillsTools = new SkillsTools();
331
+ skillsTools.getTools().forEach((tool) => {
332
+ this.tools.set(tool.definition.name, tool);
333
+ });
202
334
  // Enrichment tools
203
335
  const enrichmentTools = new EnrichmentTools(pica);
204
336
  enrichmentTools.getTools().forEach((tool) => {
@@ -330,11 +462,6 @@ export class ToolRegistry {
330
462
  dashboardTools.getTools().forEach((tool) => {
331
463
  this.tools.set(tool.definition.name, tool);
332
464
  });
333
- // Integrations tools (connection status)
334
- const integrationsTools = new IntegrationsTools(pica);
335
- integrationsTools.getTools().forEach((tool) => {
336
- this.tools.set(tool.definition.name, tool);
337
- });
338
465
  // Settings tools (credits history, storage, org profile)
339
466
  const settingsTools = new SettingsTools(pica);
340
467
  settingsTools.getTools().forEach((tool) => {
@@ -377,6 +504,21 @@ export class ToolRegistry {
377
504
  myReportedIssuesTools.getTools().forEach((tool) => {
378
505
  this.tools.set(tool.definition.name, tool);
379
506
  });
507
+ // My-recent-questions tool (ADR-226 Phase 5 — pica_my_recent_questions)
508
+ // — user-scoped read of the Phase 4 substrate (assistant_interactions
509
+ // joined with mcp_sessions for client identity). Atlas-resolved entry
510
+ // for "what did I ask the AI?".
511
+ //
512
+ // ADR-230 post-ship cleanup — the tier resolver is the registry's
513
+ // own `getToolTier` shape (declared `tier` on each tool definition).
514
+ // Lint Rule 13 makes tier required for every customer MCP tool, so
515
+ // this resolver returns a populated value for all known tools and
516
+ // `undefined` only for unknown / meta tools — `dominantTier` falls
517
+ // back to "read" in that case.
518
+ const myRecentQuestionsTools = new MyRecentQuestionsTools(pica, (name) => this.tools.get(name)?.definition.tier);
519
+ myRecentQuestionsTools.getTools().forEach((tool) => {
520
+ this.tools.set(tool.definition.name, tool);
521
+ });
380
522
  // Agent identity tools (ADR-185 Part 1 — session-auth-only MCP surface).
381
523
  // The three tools refuse grant-auth callers at the HTTP API layer;
382
524
  // the stdio path carries an API key so behaviour is identical either
@@ -395,6 +537,21 @@ export class ToolRegistry {
395
537
  shareLinksTools.getTools().forEach((tool) => {
396
538
  this.tools.set(tool.definition.name, tool);
397
539
  });
540
+ // Sharing trace tool (ADR-232 § Decision 2)
541
+ const sharingTools = new SharingTools(pica);
542
+ sharingTools.getTools().forEach((tool) => {
543
+ this.tools.set(tool.definition.name, tool);
544
+ });
545
+ // Explainability tools (ADR-232 § Decision 3)
546
+ const explainabilityTools = new ExplainabilityTools(pica);
547
+ explainabilityTools.getTools().forEach((tool) => {
548
+ this.tools.set(tool.definition.name, tool);
549
+ });
550
+ // Access simulator — read-only "who would see what?" preview.
551
+ const accessSimulateTools = new AccessSimulateTools(pica);
552
+ accessSimulateTools.getTools().forEach((tool) => {
553
+ this.tools.set(tool.definition.name, tool);
554
+ });
398
555
  // Disputes tools (ADR-139 Step 3)
399
556
  const disputesTools = new DisputesTools(pica);
400
557
  disputesTools.getTools().forEach((tool) => {
@@ -456,72 +613,15 @@ export class ToolRegistry {
456
613
  });
457
614
  }
458
615
  }
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";
616
+ /**
617
+ * ADR-230 declared tier lookup by tool name. Used by the HTTP MCP
618
+ * dispatcher (`app/api/mcp/route.ts`) to enforce the elevated
619
+ * `destructive:*` scope on top of the resource-level scope from
620
+ * `lib/services/mcp-scopes`. Returns undefined for unknown tools or
621
+ * tools that haven't declared tier yet (legacy tolerance).
622
+ */
623
+ getToolTier(name) {
624
+ return this.tools.get(name)?.definition.tier;
525
625
  }
526
626
  /**
527
627
  * List all available tools with write-safety prefixes injected.
@@ -543,9 +643,14 @@ export class ToolRegistry {
543
643
  return toolsToList.map((tool) => {
544
644
  let definition = tool.definition;
545
645
  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");
646
+ // ADR-230 declared tier is the source of truth for hints + risk.
647
+ // Lint Rule 13 enforces tier presence on every customer MCP tool;
648
+ // metadata.risk is a quiet fallback for the unlikely case of a tool
649
+ // that bypasses lint.
650
+ const declaredTier = definition.tier;
651
+ const isDestructive = declaredTier
652
+ ? declaredTier === "destructive"
653
+ : metadata?.risk === "destructive";
549
654
  if (isDestructive) {
550
655
  definition = {
551
656
  ...definition,
@@ -573,6 +678,26 @@ export class ToolRegistry {
573
678
  },
574
679
  };
575
680
  }
681
+ // ADR-230 — derive hints from declared tier when present (every
682
+ // tool registered in mcp-server/src/tools/*.ts has tier as of
683
+ // PR-1; the metadata branch stays as a defensive code path).
684
+ if (declaredTier && metadata) {
685
+ return {
686
+ ...definition,
687
+ description: injectMetadataIntoDescription(tool.definition.description, metadata),
688
+ annotations: {
689
+ title: metadata.display_name,
690
+ readOnlyHint: declaredTier === "read",
691
+ destructiveHint: declaredTier === "destructive",
692
+ idempotentHint: declaredTier !== "destructive" && metadata.retry_safe,
693
+ openWorldHint: false,
694
+ // ADR-199 extended annotation classes — read by ChatGPT/Claude.ai
695
+ // connectors directly. Derived from declared tier so the four
696
+ // surfaces (hint, riskLevel, audit row, scope) all agree.
697
+ ...this.deriveExtendedAnnotations(definition, metadata, undefined, declaredTier),
698
+ },
699
+ };
700
+ }
576
701
  if (metadata) {
577
702
  return {
578
703
  ...definition,
@@ -583,25 +708,25 @@ export class ToolRegistry {
583
708
  destructiveHint: metadata.risk === "destructive",
584
709
  idempotentHint: metadata.risk !== "destructive" && metadata.retry_safe,
585
710
  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
711
  ...this.deriveExtendedAnnotations(definition, metadata),
590
712
  },
591
713
  };
592
714
  }
593
- // Fallback to old pattern-matching for any tool without explicit metadata
594
- const classification = this.classifyTool(definition.name);
715
+ // ADR-230 tools without a TOOL_METADATA entry (the 5 lobby
716
+ // meta-tools sign_in/out/discover/tool_details/execute) still
717
+ // declare tier. Source hints from declared tier; default to read
718
+ // when tier is somehow missing (lint blocks; defensive only).
719
+ const tierForHints = declaredTier ?? "read";
595
720
  const annotations = {
596
721
  title: definition.name,
597
- readOnlyHint: classification === "safe",
598
- destructiveHint: classification === "destructive",
722
+ readOnlyHint: tierForHints === "read",
723
+ destructiveHint: tierForHints === "destructive",
599
724
  idempotentHint: false,
600
725
  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),
726
+ // ADR-199: derive the extended classes from declared tier so the
727
+ // category/risk surface stays aligned with the rest of the
728
+ // hint/audit/scope chain.
729
+ ...this.deriveExtendedAnnotations(definition, null, undefined, tierForHints),
605
730
  };
606
731
  // No risk prefix needed — annotations handle this structurally
607
732
  return { ...definition, annotations };
@@ -616,9 +741,16 @@ export class ToolRegistry {
616
741
  * edits required for the retrofit. Future tool authors can override by
617
742
  * setting fields directly on definition.annotations.
618
743
  */
619
- deriveExtendedAnnotations(definition, metadata, classification) {
744
+ deriveExtendedAnnotations(definition, metadata, _classification, declaredTier) {
620
745
  const name = definition.name;
621
- const risk = metadata?.risk ?? classification ?? "safe";
746
+ // ADR-230 declared tier is the source of truth. metadata.risk
747
+ // remains a quiet fallback for any tool that bypasses lint Rule 13.
748
+ // (The `_classification` parameter is retained as an unused
749
+ // positional slot so legacy call signatures continue to compile;
750
+ // its value is now ignored — classifyTool was removed in PR-3.)
751
+ const risk = declaredTier
752
+ ? tierToRiskLevel(declaredTier)
753
+ : (metadata?.risk ?? "safe");
622
754
  // Side-effecting external systems: telegram, send-hub, comms, broadcasts.
623
755
  // Pattern-derived so new tools inherit without edits.
624
756
  const externalSideEffectPatterns = [
@@ -638,10 +770,16 @@ export class ToolRegistry {
638
770
  // the agent wants to confirm — the gap report flagged this as a useful
639
771
  // class for ChatGPT to surface in the UI.
640
772
  const requiresFollowUpInspection = risk === "mutating" || risk === "destructive";
773
+ // ADR-230 — destructive tier always requires confirmation. Other tiers
774
+ // never do via this annotation (per-tool opt-in for `write` tier is
775
+ // expressed through `previewMode: "two_step_token"`, not here).
776
+ const requiresConfirmation = declaredTier
777
+ ? declaredTier === "destructive"
778
+ : risk === "destructive";
641
779
  return {
642
780
  riskLevel: risk,
643
781
  category: metadata?.category,
644
- requiresConfirmation: risk === "destructive",
782
+ requiresConfirmation,
645
783
  createsExternalSideEffects,
646
784
  returnsPaginated,
647
785
  requiresFollowUpInspection,
@@ -677,6 +815,15 @@ export class ToolRegistry {
677
815
  display_name: displayName,
678
816
  };
679
817
  }
818
+ case "pica_works_bulk_move_to_performances": {
819
+ const count = args.ids?.length || 0;
820
+ return {
821
+ action: "bulk reclassify works → performances",
822
+ target: `${count} ${count === 1 ? "work" : "works"}`,
823
+ 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.`,
824
+ display_name: displayName,
825
+ };
826
+ }
680
827
  case "pica_people_delete": {
681
828
  const person = await pica.people.get(args.id).catch(() => null);
682
829
  const personName = person?.first_name
@@ -689,6 +836,15 @@ export class ToolRegistry {
689
836
  display_name: displayName,
690
837
  };
691
838
  }
839
+ case "pica_people_bulk_delete": {
840
+ const count = args.ids?.length || 0;
841
+ return {
842
+ action: "bulk delete people",
843
+ target: `${count} ${count === 1 ? "person" : "people"}`,
844
+ warning: `this will soft-delete ${count} person record(s). credits and agreements remain linked to the removed records.`,
845
+ display_name: displayName,
846
+ };
847
+ }
692
848
  case "pica_agreements_delete": {
693
849
  const agreementResult = await pica.agreements
694
850
  .get(args.id)
@@ -834,18 +990,11 @@ export class ToolRegistry {
834
990
  }
835
991
  }
836
992
  /**
837
- * Sanitize tool parameters for audit logging.
838
- * Strips confirmation tokens and truncates large string values.
993
+ * Sanitize tool parameters for audit logging — delegates to the
994
+ * pure-function impl below so it can be exercised directly in tests.
839
995
  */
840
996
  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;
997
+ return sanitizeAuditParams(args);
849
998
  }
850
999
  /**
851
1000
  * ADR-208 Primitive B — opaque per-org cache key.
@@ -1018,10 +1167,22 @@ export class ToolRegistry {
1018
1167
  throw new ToolExecutionError(`Tool not found: ${name}`);
1019
1168
  }
1020
1169
  const metadata = getToolMetadata(name);
1170
+ // ADR-230 — declared tier is the source of truth for the freshness +
1171
+ // confirmation gates and the audit-log surfaces. Fall back to
1172
+ // metadata.risk only if the tool somehow lacks tier (lint blocks
1173
+ // missing tier on creator + team + directory MCPs as of PR-2).
1174
+ const declaredTier = tool.definition.tier;
1175
+ const fallbackRisk = metadata?.risk ?? "safe";
1176
+ const auditTier = declaredTier ??
1177
+ (fallbackRisk === "safe"
1178
+ ? "read"
1179
+ : fallbackRisk === "destructive"
1180
+ ? "destructive"
1181
+ : "write");
1182
+ const auditRiskLevel = tierToRiskLevel(auditTier);
1183
+ const isDestructive = auditTier === "destructive";
1021
1184
  // ── Session freshness gate (ADR-151) ──
1022
- if (metadata?.risk === "destructive" &&
1023
- this.config &&
1024
- !this.config.lobbyMode) {
1185
+ if (isDestructive && this.config && !this.config.lobbyMode) {
1025
1186
  const creds = readCredentials(this.config.credentialsPath);
1026
1187
  if (creds?.authenticated_at) {
1027
1188
  const authAge = Date.now() - new Date(creds.authenticated_at).getTime();
@@ -1039,12 +1200,15 @@ export class ToolRegistry {
1039
1200
  }
1040
1201
  }
1041
1202
  }
1042
- // ── Destructive confirmation gate ──
1043
- if (metadata?.risk === "destructive") {
1203
+ // ── Destructive confirmation gate (ADR-230 — keyed on declared tier) ──
1204
+ if (isDestructive) {
1044
1205
  const confirmationToken = args.confirmation_token;
1045
1206
  if (!confirmationToken) {
1046
- // First call — generate preview and return confirmation challenge
1047
- const token = generateConfirmationToken(name, args);
1207
+ // First call — generate preview and return confirmation challenge.
1208
+ // ADR-230 PR-4 — token state lives in Postgres for HTTP MCP under
1209
+ // Fluid Compute (or in-memory for stdio); both paths via the
1210
+ // active ConfirmationStore singleton, so callers stay unchanged.
1211
+ const token = await generateConfirmationToken(name, args);
1048
1212
  const preview = await this.buildDestructivePreview(name, args);
1049
1213
  const response = {
1050
1214
  content: [
@@ -1070,6 +1234,7 @@ export class ToolRegistry {
1070
1234
  ?.logToolExecution({
1071
1235
  tool_name: name,
1072
1236
  tool_category: metadata?.category || "unknown",
1237
+ tier: "destructive",
1073
1238
  risk_level: "destructive",
1074
1239
  parameters: this.sanitizeParams(args),
1075
1240
  result_status: "confirmation_required",
@@ -1077,12 +1242,14 @@ export class ToolRegistry {
1077
1242
  execution_time_ms: Date.now() - startTime,
1078
1243
  caller_identity: this.callerContext.callerIdentity,
1079
1244
  transport: this.callerContext.transport,
1245
+ ...this.extractClientInfo(ctx),
1080
1246
  })
1081
1247
  .catch(() => { }); // Never block on audit
1082
1248
  return response;
1083
1249
  }
1084
- // Second call — validate token
1085
- const validation = validateAndConsumeToken(confirmationToken, name, args);
1250
+ // Second call — validate token (Postgres-backed under Fluid Compute
1251
+ // since ADR-230 PR-4; the active store handles cross-instance state).
1252
+ const validation = await validateAndConsumeToken(confirmationToken, name, args);
1086
1253
  if (!validation.valid) {
1087
1254
  return {
1088
1255
  content: [
@@ -1115,10 +1282,10 @@ export class ToolRegistry {
1115
1282
  }
1116
1283
  }
1117
1284
  // 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";
1285
+ // Read tools (`tier: "read"`) skip this entirely so the high-volume
1286
+ // read path stays cheap. `result.isError === true` also skips because
1287
+ // a failed mutation didn't change catalog state.
1288
+ const isWriteTool = auditRiskLevel !== "safe";
1122
1289
  if (isWriteTool && result && !result.isError) {
1123
1290
  await this.attachSessionState(result, name);
1124
1291
  }
@@ -1144,12 +1311,14 @@ export class ToolRegistry {
1144
1311
  ?.logToolExecution({
1145
1312
  tool_name: name,
1146
1313
  tool_category: metadata?.category || "unknown",
1147
- risk_level: metadata?.risk || this.classifyTool(name),
1314
+ tier: auditTier,
1315
+ risk_level: auditRiskLevel,
1148
1316
  parameters: this.sanitizeParams(args),
1149
1317
  result_status: "success",
1150
1318
  execution_time_ms: Date.now() - startTime,
1151
1319
  caller_identity: this.callerContext.callerIdentity,
1152
1320
  transport: this.callerContext.transport,
1321
+ ...this.extractClientInfo(ctx),
1153
1322
  })
1154
1323
  .catch(() => { }); // Never block on audit
1155
1324
  return result;
@@ -1170,13 +1339,19 @@ export class ToolRegistry {
1170
1339
  ?.logToolExecution({
1171
1340
  tool_name: name,
1172
1341
  tool_category: metadata?.category || "unknown",
1173
- risk_level: metadata?.risk || this.classifyTool(name),
1342
+ tier: auditTier,
1343
+ risk_level: auditRiskLevel,
1174
1344
  parameters: this.sanitizeParams(args),
1175
1345
  result_status: "error",
1176
1346
  error_code: parsed.error,
1347
+ // Stage A — forward the parsed body's message so mcp_audit_log
1348
+ // captures the underlying cause. Without this every error row had
1349
+ // error_message=null, leaving UNKNOWN_ERROR rows opaque.
1350
+ error_message: typeof parsed.message === "string" ? parsed.message : undefined,
1177
1351
  execution_time_ms: Date.now() - startTime,
1178
1352
  caller_identity: this.callerContext.callerIdentity,
1179
1353
  transport: this.callerContext.transport,
1354
+ ...this.extractClientInfo(ctx),
1180
1355
  })
1181
1356
  .catch(() => { }); // Never block on audit
1182
1357
  return {