@adrata/adrata-mcp 1.0.19 → 1.0.30

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/access/tiers.js CHANGED
@@ -27,6 +27,12 @@ export const TOOL_TIERS = {
27
27
  // or reach a second workspace from the CLI at all.
28
28
  list_workspaces: TIERS.ENTERPRISE,
29
29
  switch_workspace: TIERS.ENTERPRISE,
30
+ // Provisioning a customer's workspace: reading who is seated, and seating
31
+ // somebody with the onboarding link that reaches them. Enterprise because it
32
+ // is workspace administration -- the API separately requires the connected
33
+ // user to be a workspace admin and answers 403 otherwise.
34
+ list_workspace_seats: TIERS.ENTERPRISE,
35
+ create_onboarding_link: TIERS.ENTERPRISE,
30
36
  adrata_api_catalog: TIERS.FREE,
31
37
  paper_app_audit: TIERS.FREE,
32
38
  adrata_desktop_app_audit: TIERS.FREE,
@@ -220,6 +226,7 @@ export const TOOL_TIERS = {
220
226
  list_work_boards: TIERS.ENTERPRISE,
221
227
  list_source_control_connections: TIERS.ENTERPRISE,
222
228
  get_source_control_connection_events: TIERS.ENTERPRISE,
229
+ bind_repository_to_board: TIERS.ENTERPRISE,
223
230
  get_work_item_delivery_evidence: TIERS.ENTERPRISE,
224
231
  audit_work_hub: TIERS.ENTERPRISE,
225
232
  list_work_item_acceptance_criteria: TIERS.ENTERPRISE,
@@ -258,6 +265,11 @@ export const TOOL_TIERS = {
258
265
  unsatisfy_work_item_acceptance_criterion: TIERS.ENTERPRISE,
259
266
  comment_on_work_item: TIERS.ENTERPRISE,
260
267
  flag_work_item: TIERS.ENTERPRISE,
268
+ // Retiring a card and bringing it back. Same reasoning as every board write:
269
+ // these change a real workspace record.
270
+ archive_work_item: TIERS.ENTERPRISE,
271
+ delete_work_item: TIERS.ENTERPRISE,
272
+ unarchive_work_item: TIERS.ENTERPRISE,
261
273
  // The containers above the cards, and the "add this to the roadmap" verb.
262
274
  // Same reasoning as every board tool: scopes are real workspace records.
263
275
  list_work_scopes: TIERS.ENTERPRISE,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@adrata/adrata-mcp",
3
- "version": "1.0.19",
4
- "description": "Adrata MCP Server \u2014 connect Claude Code, Codex, Gemini, Cursor, and other AI tools to your CRM. 80+ tools for companies, people, deals, actions, buyer groups, warm intros, webhooks, intelligence, and more.",
3
+ "version": "1.0.30",
4
+ "description": "Adrata MCP Server \u2014 connect Claude Code, Codex, Gemini, Cursor, and other AI tools to your CRM. About 275 tools registered at startup for companies, people, deals, actions, buyer groups, warm intros, webhooks and intelligence, plus 65 more behind eight named toolsets you load with enable_toolset.",
5
5
  "type": "module",
6
6
  "main": "server.js",
7
7
  "bin": {
@@ -9,7 +9,7 @@
9
9
  },
10
10
  "scripts": {
11
11
  "start": "node server.js",
12
- "test": "node --test analytics.test.js server.test.js api-bridge.test.js http/edge-block.test.js http/rate-limit.test.js audit-flush.test.js buyer-group-writes.test.js note-writes.test.js mcp-spec.test.js packaging.test.js product-profile.test.js security.test.js tool-annotations.test.js toolsets.test.js toolsets/communications.test.js access/auth.test.js access/oauth-callback.test.js access/oauth-session.test.js access/oauth-capabilities.test.js access/oauth-scope-grantability.test.js scripts/local-dev-server.test.js tools/competitive-coverage.test.js tools/describe-count.test.js tools/email-tools.test.js tools/scheduling.test.js tools/work-board-tools.test.js tools/work-hub/audit.test.js tools/work-hub/criteria-quality.test.js tools/roadmap-tools.test.js tools/source-control/connection-tools.test.js governance/money.test.js http/rate-limit-policy.test.js"
12
+ "test": "node --test analytics.test.js server.test.js api-bridge.test.js http/edge-block.test.js http/rate-limit.test.js audit-flush.test.js buyer-group-writes.test.js note-writes.test.js mcp-spec.test.js packaging.test.js product-profile.test.js security.test.js security.cap-contract.test.js tool-annotations.test.js toolsets.test.js toolsets/communications.test.js access/auth.test.js access/oauth-callback.test.js access/oauth-session.test.js access/oauth-capabilities.test.js access/oauth-scope-grantability.test.js scripts/local-dev-server.test.js tools/competitive-coverage.test.js tools/describe-count.test.js tools/email-tools.test.js tools/scheduling.test.js tools/work-board-tools.test.js tools/work-hub/audit.test.js tools/work-hub/criteria-quality.test.js tools/roadmap-tools.test.js tools/provisioning/onboarding-tools.test.js tools/source-control/connection-tools.test.js governance/money.test.js http/rate-limit-policy.test.js"
13
13
  },
14
14
  "keywords": [
15
15
  "mcp",
package/security.js CHANGED
@@ -143,18 +143,169 @@ export function rateLimitResponse(retryAfterMs) {
143
143
  // 2. Input Validation
144
144
  // ---------------------------------------------------------------------------
145
145
 
146
+ /**
147
+ * Length caps for TOP-LEVEL string tool arguments, by argument name.
148
+ *
149
+ * `applySecurityLayer` wraps every tool this server registers, so these caps
150
+ * are the FIRST thing any argument meets — before Zod, before the handler,
151
+ * before the API. Until 2026-09-05 they TRUNCATED, silently, and the tool then
152
+ * returned success: a card flag ended mid-word at exactly 1000 characters
153
+ * ("Do not substitute API SHA or bounce the card merely t") and the
154
+ * prohibition was gone for every subsequent reader including its author.
155
+ *
156
+ * Every comparable cap in the Rust API refuses instead — comments 2000,
157
+ * criterion parts 600, engineering-proof fields 600, reviewer model 120,
158
+ * evidence summary 600 — several enforced twice, once in the handler and
159
+ * again as a database CHECK. So every caller already handles a refusal. This
160
+ * layer was the only one that cut, and it ran first, which is why the cut was
161
+ * invisible from both ends.
162
+ */
163
+ const DEFAULT_STRING_CAP = 1000;
164
+
165
+ /**
166
+ * The API's own cap on a card comment, and on every free-text `reason` that
167
+ * routes through `validate_body`: flag, block, dependency, sharing, QA-flow
168
+ * note. Rust calls it `MAX_COMMENT_BODY_CHARS`
169
+ * (`code/api/crates/routes-gtm/src/work_boards/mod.rs`).
170
+ *
171
+ * `security.cap-contract.test.js` reads that constant out of the Rust
172
+ * source and fails if this number ever drops below it, because THAT is the
173
+ * divergence this whole change is about: a cap here that sits below the cap
174
+ * downstream cuts a value the API would have accepted, and the API never gets
175
+ * to refuse it.
176
+ */
177
+ const API_COMMENT_BODY_CAP = 2000;
178
+
179
+ /**
180
+ * Sanity ceiling for an argument that carries a document or a data file rather
181
+ * than prose (a CSV to import, a knowledge file's markdown, a paper's HTML).
182
+ * Deliberately far above any product rule: the point is to bound the transport,
183
+ * and to let the API be the layer that says how big a document may be.
184
+ */
185
+ const BULK_PAYLOAD_CAP = 1_000_000;
186
+
187
+ const STRING_CAPS_BY_KEY = {
188
+ // Long-form prose the API caps far lower anyway (a comment body is refused
189
+ // at 2000). 10000 here is a sanity ceiling, not the product rule.
190
+ body: 10000,
191
+ message: 10000,
192
+ description: 10000,
193
+
194
+ // The reason family. These were the DANGEROUS truncations and the reason
195
+ // this cap exists as its own class:
196
+ //
197
+ // flag_work_item.reason API cap 2000 — a 1000-cut value was
198
+ // ACCEPTED truncated. This is the flag
199
+ // defect: a prohibition ended mid-word at
200
+ // exactly 1000 characters and read as
201
+ // complete to every later reader.
202
+ // block_work_item.reason API cap 2000 — same shape.
203
+ // move_work_item.reason NO API cap at all, so nothing downstream
204
+ // transfer_...between_boards could ever catch the cut. Permanently and
205
+ // silently shortened to 1000.
206
+ // every X-Adrata-Reason header NO API cap anywhere in Rust
207
+ // (api-bridge.js) (presence-only). Same silence.
208
+ //
209
+ // Raising these to the API's own 2000 is what keeps this change from
210
+ // trading a silent cut for a refusal of honest input: a 1400-character
211
+ // reason is legitimate, the API accepts it, and until now this layer cut it.
212
+ reason: API_COMMENT_BODY_CAP,
213
+
214
+ // `auditReason` is the same field family under a different name: the shared
215
+ // `moneyWrite` envelope (server.js) spreads it into EVERY money write, so it
216
+ // was the one reason-shaped argument the first version of this table missed.
217
+ auditReason: API_COMMENT_BODY_CAP,
218
+
219
+ // `record_work_item_qa_failure_and_release.details` is composed into a flag
220
+ // reason (`"QA criterion failure (<id>): " + details`) and so lands under
221
+ // the same 2000 API cap. A 1000-character cut plus that ~14-character prefix
222
+ // stayed comfortably under 2000, which is exactly why the "~1,014 characters,
223
+ // 200 OK" measurement was silent. Known rough edge: a `details` near 2000
224
+ // composes to over 2000 and the API refuses the composed value with its own
225
+ // message. That is a refusal rather than a silence, which is the property
226
+ // this change is about.
227
+ details: API_COMMENT_BODY_CAP,
228
+
229
+ // Search terms. A query longer than this is a paste, not a search.
230
+ query: 500,
231
+ search: 500,
232
+
233
+ // --- Payloads, not prose ---
234
+ //
235
+ // These carry a document or a data file, and at the old 1000 default they
236
+ // were the WORST instances of the truncation: `bulk_import`/`manage_data`
237
+ // took a CSV — any real one is past 1000 in its first few rows — cut it to
238
+ // 1000, imported the fragment, and reported success. A partial import that
239
+ // announces itself as complete is harder to notice than a lost sentence.
240
+ //
241
+ // The ceiling here is a transport sanity bound, NOT a product rule. This
242
+ // layer has no business deciding how large a knowledge file may be; the API
243
+ // owns that, and it can refuse. What this layer must never again do is
244
+ // decide it silently.
245
+ csvData: BULK_PAYLOAD_CAP,
246
+ content: BULK_PAYLOAD_CAP,
247
+ };
248
+
249
+ /** The cap that applies to a top-level string argument named `key`. */
250
+ export function maxLengthForKey(key) {
251
+ return Object.prototype.hasOwnProperty.call(STRING_CAPS_BY_KEY, key)
252
+ ? STRING_CAPS_BY_KEY[key]
253
+ : DEFAULT_STRING_CAP;
254
+ }
255
+
256
+ /**
257
+ * The refusal text. It names the actual length, the cap, and how much to cut,
258
+ * because the caller is usually an agent that can shorten and retry — and it
259
+ * says plainly that nothing was written, since the whole point of the defect
260
+ * this replaces was a caller believing a truncated write had landed.
261
+ */
262
+ export function overLengthMessage(field, length, maxLength) {
263
+ return `Parameter "${field}" is ${length} characters and the cap is ${maxLength} — `
264
+ + `remove ${length - maxLength}. Nothing was written: the value was refused whole `
265
+ + `rather than cut, so shorten it and call again.`;
266
+ }
267
+
146
268
  /**
147
269
  * Common validation schemas for tool parameters.
148
270
  * These wrap and tighten the existing Zod schemas.
149
271
  */
150
272
  export const validators = {
151
- /** Sanitize a string — strip control characters, limit length. */
152
- sanitizeString(value, maxLength = 1000) {
153
- if (typeof value !== 'string') return value;
154
- // Strip control characters except newline/tab
155
- // eslint-disable-next-line no-control-regex
156
- const cleaned = value.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '');
157
- return cleaned.slice(0, maxLength);
273
+ /**
274
+ * Strip control characters, and REFUSE a string longer than `maxLength`.
275
+ *
276
+ * Returns `{ value, error }` rather than a bare string on purpose. Until
277
+ * 2026-09-05 this ended `return cleaned.slice(0, maxLength)` — the value was
278
+ * cut, `validateToolInput` still reported `valid: true`, and the write went
279
+ * on to return 200. The object shape means a caller cannot use the result as
280
+ * a string without noticing, so the refusal cannot be dropped the way the
281
+ * silent slice was.
282
+ *
283
+ * Length is counted in CODE POINTS (`[...cleaned].length`), matching the
284
+ * Rust API's `chars().count()` downstream rather than UTF-16 code units, so
285
+ * a value this layer accepts is not one the next layer measures differently.
286
+ */
287
+ sanitizeString(value, maxLength = DEFAULT_STRING_CAP, field = 'value') {
288
+ if (typeof value !== 'string') return { value, error: null };
289
+ // Vertical tab (0x0B) and form feed (0x0C) ARE whitespace: they separate
290
+ // words. Deleting them outright fuses the tokens either side into one —
291
+ // "evidence<VT>01M1RAMY" becomes "evidence01M1RAMY" — and that is worse to
292
+ // read than a truncation, because the sentence still looks complete, so a
293
+ // reader has no tell at all. They collapse to a space. Every other control
294
+ // character in the class is not a separator, so removing it stays correct.
295
+ //
296
+ // Tab (0x09), newline (0x0A) and carriage return (0x0D) sit outside both
297
+ // classes and pass through untouched, as they always have.
298
+ const cleaned = value
299
+ // eslint-disable-next-line no-control-regex
300
+ .replace(/[\x0B\x0C]/g, ' ')
301
+ // eslint-disable-next-line no-control-regex
302
+ .replace(/[\x00-\x08\x0E-\x1F\x7F]/g, '');
303
+ // Measured AFTER stripping: control characters must not eat the budget.
304
+ const length = [...cleaned].length;
305
+ if (length > maxLength) {
306
+ return { value: cleaned, error: overLengthMessage(field, length, maxLength) };
307
+ }
308
+ return { value: cleaned, error: null };
158
309
  },
159
310
 
160
311
  /** Validate that a string doesn't contain obvious injection patterns. */
@@ -223,13 +374,15 @@ export function validateToolInput(toolName, args) {
223
374
  continue;
224
375
  }
225
376
 
226
- // Sanitize string values
227
- const maxLen = key === 'body' || key === 'message' || key === 'description'
228
- ? 10000
229
- : key === 'query' || key === 'search'
230
- ? 500
231
- : 1000;
232
- sanitized[key] = validators.sanitizeString(value, maxLen);
377
+ // Sanitize string values. An over-long value is REFUSED, not cut:
378
+ // see the cap table above for why this layer used to truncate silently.
379
+ const { value: cleaned, error: lengthError } =
380
+ validators.sanitizeString(value, maxLengthForKey(key), key);
381
+ if (lengthError) {
382
+ errors.push(lengthError);
383
+ continue;
384
+ }
385
+ sanitized[key] = cleaned;
233
386
 
234
387
  // Validate specific field types
235
388
  if (key === 'id' || key.endsWith('Id')) {
package/server.js CHANGED
@@ -55,6 +55,7 @@ import { registerEmailTools } from './tools/email-tools.js';
55
55
  import { registerWorkBoardTools } from './tools/work-board-tools.js';
56
56
  import { registerSourceControlTools } from './tools/source-control/connection-tools.js';
57
57
  import { registerRoadmapTools } from './tools/roadmap-tools.js';
58
+ import { registerProvisioningTools } from './tools/provisioning/onboarding-tools.js';
58
59
  import { registerPaperTools } from './tools/paper-tools.js';
59
60
  import { register as registerAlwaysLoadedTools } from './toolsets/revenue/always-loaded.js';
60
61
  import { register as registerExtensibilityTools } from './toolsets/extensibility.js';
@@ -2394,7 +2395,29 @@ registerWorkBoardTools(server, {
2394
2395
  getGrantedScope: () => loadTokens()?.scope,
2395
2396
  });
2396
2397
 
2397
- registerSourceControlTools(server, { z, api, ok });
2398
+ registerSourceControlTools(server, {
2399
+ z,
2400
+ api,
2401
+ ok,
2402
+ validateApiBridgeRequest,
2403
+ buildMutationHeaders,
2404
+ getGrantedScope: () => loadTokens()?.scope,
2405
+ });
2406
+
2407
+ // Seating a customer's people and minting the onboarding link an AE hands them.
2408
+ // The first path from MCP that can actually finish: `invite_user` through
2409
+ // `adrata_ai_tool_execute` refuses every machine principal at the confirmation
2410
+ // step (ai_crm_tools/mod.rs:427), so the bridge could preview a seat forever and
2411
+ // create none. See tools/provisioning.js for why this is one tool and not two.
2412
+ registerProvisioningTools(server, {
2413
+ z,
2414
+ api,
2415
+ ok,
2416
+ validateApiBridgeRequest,
2417
+ buildMutationHeaders,
2418
+ getGrantedScope: () => loadTokens()?.scope,
2419
+ getWorkspaceId: () => loadTokens()?.workspaceId ?? AUTH?.workspaceId ?? null,
2420
+ });
2398
2421
 
2399
2422
  // The containers above the cards, and the "add this to the roadmap" verb
2400
2423
  // (company/decisions/2026-08-06-spoq-roadmap-sync.md).
package/server.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "name": "com.adrata/adrata-mcp",
4
4
  "description": "Adrata revenue-intelligence MCP server: companies, people, opportunities, actions, buyer groups, enrichment, email, and workspace operations for AI agents.",
5
5
  "status": "active",
6
- "version": "1.0.19",
6
+ "version": "1.0.30",
7
7
  "websiteUrl": "https://adrata.com/developers",
8
8
  "repository": {
9
9
  "url": "https://github.com/adrata/adrata",
@@ -15,7 +15,7 @@
15
15
  "registryType": "npm",
16
16
  "registryBaseUrl": "https://registry.npmjs.org",
17
17
  "identifier": "@adrata/adrata-mcp",
18
- "version": "1.0.19",
18
+ "version": "1.0.30",
19
19
  "transport": {
20
20
  "type": "stdio"
21
21
  },
@@ -13,7 +13,14 @@ The workflow semantics are fixed by
13
13
  `company/decisions/2026-08-29-two-gate-ai-qa-human-production.md`; the required
14
14
  evidence loop is fixed by
15
15
  `company/decisions/2026-08-29-recorded-staging-qa-standard.md`. This skill is
16
- their operating procedure.
16
+ their operating procedure. Read the current amendment at
17
+ `company/decisions/2026-09-04-complete-quality-gates-and-instant-interactions.md`
18
+ and the card contract at
19
+ `company/playbooks/engineering/card-readiness-and-evidence-contract.md`.
20
+ Every required criterion/configuration must pass: failed, blocked, skipped,
21
+ missing, stale, unknown, or flaky results block completion. Do not use a human
22
+ acknowledgement or legacy unknown-author exception to clear an incomplete gate.
23
+ No visible UI changes are authorized by the quality audit.
17
24
 
18
25
  ## Establish the real queue
19
26
 
@@ -38,6 +38,13 @@ const DOMAIN_OVERRIDES = {
38
38
  who_am_i: 'workspace',
39
39
  get_current_user: 'workspace',
40
40
 
41
+ // Provisioning: seating a user and minting the onboarding link that reaches
42
+ // them. Categorised 'workspace' rather than given a category of their own —
43
+ // these are workspace administration, and the profile that can administer a
44
+ // workspace is the one that should see them.
45
+ list_workspace_seats: 'workspace',
46
+ create_onboarding_link: 'workspace',
47
+
41
48
  // Governed API bridge.
42
49
  // Broad backwards-compatible escape hatches stay on the unprofiled Adrata
43
50
  // server. Product profiles use the namespace-bound capability bridge below.
@@ -241,10 +248,20 @@ const IDEMPOTENT_WRITES = new Set([
241
248
  // comment, criterion, or card.
242
249
  'move_work_item', 'transfer_work_item_between_boards', 'set_work_board_column_wip_limit', 'set_work_item_tag', 'set_work_item_kind',
243
250
  'create_work_item', 'comment_on_work_item', 'flag_work_item',
251
+ // Retiring an already-retired card is REFUSED with a 409 rather than
252
+ // replayed, so the reason of the first retirement cannot be silently
253
+ // overwritten — which makes a retry with the same key land on the same state.
254
+ // The restore is a plain clear of the same five columns.
255
+ 'archive_work_item', 'unarchive_work_item',
244
256
  // A repeat block is the same edge (the pair is unique), and a repeat
245
257
  // unblock deletes a row that is already gone. Neither compounds.
246
258
  'block_work_item', 'unblock_work_item',
247
259
  'add_work_item_acceptance_criterion',
260
+ // (delete_work_item needs no entry here either: DESTRUCTIVE_PATTERN matches
261
+ // the `delete_` prefix, so a host confirmation-gates it on its NAME. That is
262
+ // the whole reason the destructive branch is a separate tool rather than a
263
+ // `kind` argument on archive_work_item — a switch inside a safe-sounding name
264
+ // would be gated by nothing.)
248
265
  // Reclassifying is a PUT of the whole routing decision, not an append:
249
266
  // replaying the same key rewrites the criterion to the same route and the
250
267
  // server's receipt table records one change, never a second one.