@extrovert.dev/mcp 0.1.0-pre.5 → 0.1.0-pre.6

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/dist/tools.js CHANGED
@@ -12,6 +12,7 @@
12
12
  * an org-wide master key. `redeem_enrollment` lets an agent exchange a
13
13
  * single-use enrollment token for that scoped key at runtime.
14
14
  */
15
+ import { createHash } from "node:crypto";
15
16
  import { z } from "zod/v4";
16
17
  import { ExtrovertApiError } from "./client.js";
17
18
  import { isTerminalReviewEvent } from "./types.js";
@@ -454,9 +455,12 @@ const signUp = defineTool({
454
455
  name: "sign_up",
455
456
  title: "Sign up for a free account",
456
457
  description: "Grab a free Extrovert account in one call (no enrollment token needed). Provisions a tenant and a first inbox, then " +
457
- "emails a one-time verification code to your human_email. Returns a LIMITED (read-only) agent key that this " +
458
+ "emails a one-time verification code to your human_email. Returns a LIMITED verification-only agent key that " +
459
+ "cannot read or send mail and that this " +
458
460
  "MCP session keeps only through verify_signup; it expires with the code and is revoked after verification. " +
459
- "Re-calling with the same human_email rotates the bootstrap key and resends the code.",
461
+ "Re-calling with the same human_email rotates the bootstrap key and resends the code. Free signup may be " +
462
+ "temporarily paused; in that state the tool " +
463
+ "returns signup_disabled and creates nothing. Enrollment tokens remain available.",
460
464
  inputSchema: {
461
465
  human_email: emailAddress.describe("Your email — receives the one-time verification code."),
462
466
  username: z
@@ -486,7 +490,9 @@ const verifySignup = defineTool({
486
490
  title: "Verify signup code",
487
491
  description: "Confirm the one-time code emailed by sign_up. On success the bootstrap key is revoked and you receive a NEW " +
488
492
  "full-scope agent key (create/read/send/webhooks). This MCP session switches to it automatically. The packaged " +
489
- "local stdio server also stores it in its permission-restricted credential file for future sessions.",
493
+ "local stdio server also stores it in its permission-restricted credential file for future sessions. Pending " +
494
+ "signup verification returns signup_disabled while free signup is temporarily paused; keep the limited key and " +
495
+ "retry after reopening.",
490
496
  inputSchema: {
491
497
  otp: z.string().min(4).max(12).describe("The verification code from your signup email."),
492
498
  },
@@ -681,6 +687,7 @@ const exportEmailConfig = defineTool({
681
687
  name: "export_email_config",
682
688
  title: "Export email client config",
683
689
  description: "Export an inbox's IMAP/SMTP server settings + login so you can configure a real mail client (e.g. Himalaya). " +
690
+ "Requires the dedicated mailbox:credentials scope and a paid plan; free accounts cannot export raw credentials. " +
684
691
  "Direct SMTP is an explicit unreviewed delivery path: it bypasses Extrovert approval/review, suppression and " +
685
692
  "contact-list enforcement, List-Unsubscribe injection, and Extrovert billing/accounting. API/MCP sends keep those " +
686
693
  "controls. Returns a ready-to-use config; use format=json for raw connection fields.",
@@ -781,6 +788,7 @@ const sendEmail = defineTool({
781
788
  .describe("Your confidence (0..1) in the category match. Feeds the min_confidence auto-send gate ONLY; " +
782
789
  "the server never scores. Below the threshold (or omitted when one is set) the would-be auto-send " +
783
790
  "routes to needs_review (gate_outcome held:low_confidence)."),
791
+ composition_token: z.string().min(1).optional().describe("Token returned by the fresh get_rules call used to compose this message. The MCP refreshes it when omitted for compatibility."),
784
792
  client_id: z
785
793
  .string()
786
794
  .min(1)
@@ -790,6 +798,9 @@ const sendEmail = defineTool({
790
798
  },
791
799
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
792
800
  handler: async (args, { client }) => {
801
+ const compositionToken = args.composition_token ?? (await client.getRules({ category_id: args.category_id })).composition_token;
802
+ if (!compositionToken)
803
+ throw new ExtrovertApiError("Call get_rules without a scope filter before composing.", 422, "composition_token_required");
793
804
  // Review Loop overload: any of mode/intent/category_id opts into the richer
794
805
  // discriminated response. It does NOT decide whether a human sees the message —
795
806
  // the policy does that either way; this only shapes what comes back.
@@ -813,6 +824,7 @@ const sendEmail = defineTool({
813
824
  intent: args.intent,
814
825
  category_id: args.category_id,
815
826
  category_confidence: args.category_confidence,
827
+ composition_token: compositionToken,
816
828
  client_id: args.client_id,
817
829
  });
818
830
  return ok(renderSubmitResult(result), result);
@@ -828,6 +840,7 @@ const sendEmail = defineTool({
828
840
  reply_to: args.reply_to,
829
841
  headers: args.headers,
830
842
  attachments: args.attachments,
843
+ composition_token: compositionToken,
831
844
  client_id: args.client_id,
832
845
  });
833
846
  return ok(renderSendOutcome("Sent", result), result);
@@ -878,6 +891,7 @@ const replyEmail = defineTool({
878
891
  .max(1)
879
892
  .optional()
880
893
  .describe("Your confidence (0..1) in the category match. Feeds the min_confidence auto-send gate only."),
894
+ composition_token: z.string().min(1).optional().describe("Token returned by the fresh get_rules call used to compose this reply. The MCP refreshes it when omitted for compatibility."),
881
895
  client_id: z
882
896
  .string()
883
897
  .min(1)
@@ -890,6 +904,9 @@ const replyEmail = defineTool({
890
904
  if (!args.thread_id && !args.message_id) {
891
905
  throw new ExtrovertApiError("Provide thread_id or message_id to reply.", 400, "invalid_argument");
892
906
  }
907
+ const compositionToken = args.composition_token ?? (await client.getRules({ category_id: args.category_id })).composition_token;
908
+ if (!compositionToken)
909
+ throw new ExtrovertApiError("Call get_rules without a scope filter before composing.", 422, "composition_token_required");
893
910
  // Review Loop overload: any of mode/intent/category_id opts into review.
894
911
  if (args.mode !== undefined || args.intent !== undefined || args.category_id !== undefined) {
895
912
  const result = await client.submitReplyForReview({
@@ -908,6 +925,7 @@ const replyEmail = defineTool({
908
925
  intent: args.intent,
909
926
  category_id: args.category_id,
910
927
  category_confidence: args.category_confidence,
928
+ composition_token: compositionToken,
911
929
  client_id: args.client_id,
912
930
  });
913
931
  return ok(renderSubmitResult(result), result);
@@ -924,6 +942,7 @@ const replyEmail = defineTool({
924
942
  headers: args.headers,
925
943
  reply_all: args.reply_all,
926
944
  attachments: args.attachments,
945
+ composition_token: compositionToken,
927
946
  client_id: args.client_id,
928
947
  });
929
948
  return ok(renderSendOutcome("Replied", res), res);
@@ -968,6 +987,7 @@ const forwardEmail = defineTool({
968
987
  .max(1)
969
988
  .optional()
970
989
  .describe("Your confidence (0..1) in the category match. Feeds the min_confidence auto-send gate only."),
990
+ composition_token: z.string().min(1).optional().describe("Token returned by the fresh get_rules call used to compose this forward. The MCP refreshes it when omitted for compatibility."),
971
991
  client_id: z
972
992
  .string()
973
993
  .min(1)
@@ -977,6 +997,9 @@ const forwardEmail = defineTool({
977
997
  },
978
998
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
979
999
  handler: async (args, { client }) => {
1000
+ const compositionToken = args.composition_token ?? (await client.getRules({ category_id: args.category_id })).composition_token;
1001
+ if (!compositionToken)
1002
+ throw new ExtrovertApiError("Call get_rules without a scope filter before composing.", 422, "composition_token_required");
980
1003
  // Same opt-in predicate as send/reply: mode/intent/category_id select the
981
1004
  // richer discriminated response; the policy governs the routing regardless.
982
1005
  if (args.mode !== undefined || args.intent !== undefined || args.category_id !== undefined) {
@@ -992,6 +1015,7 @@ const forwardEmail = defineTool({
992
1015
  intent: args.intent,
993
1016
  category_id: args.category_id,
994
1017
  category_confidence: args.category_confidence,
1018
+ composition_token: compositionToken,
995
1019
  client_id: args.client_id,
996
1020
  });
997
1021
  return ok(renderSubmitResult(result), result);
@@ -1004,6 +1028,7 @@ const forwardEmail = defineTool({
1004
1028
  bcc: args.bcc,
1005
1029
  text: args.text,
1006
1030
  html: args.html,
1031
+ composition_token: compositionToken,
1007
1032
  client_id: args.client_id,
1008
1033
  });
1009
1034
  return ok(renderSendOutcome("Forwarded", res), res);
@@ -1237,6 +1262,7 @@ const submitRevision = defineTool({
1237
1262
  "redraft could never restore a file the human reviewed the message with."),
1238
1263
  built_at: z.string().optional().describe("When you built this draft (informational)."),
1239
1264
  rules_version_seen: z.number().int().optional().describe("Rule high-water this draft was composed against (born-stale basis)."),
1265
+ composition_token: z.string().min(1).optional().describe("Token returned by the fresh get_rules call used to compose this revision. The MCP refreshes it when omitted for compatibility."),
1240
1266
  client_id: z
1241
1267
  .string()
1242
1268
  .min(1)
@@ -1246,6 +1272,10 @@ const submitRevision = defineTool({
1246
1272
  },
1247
1273
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
1248
1274
  handler: async (args, { client }) => {
1275
+ const currentReview = await client.getReview(args.id);
1276
+ const compositionToken = args.composition_token ?? (await client.getRules({ category_id: currentReview.category_id })).composition_token;
1277
+ if (!compositionToken)
1278
+ throw new ExtrovertApiError("Call get_rules without a scope filter before redrafting.", 422, "composition_token_required");
1249
1279
  const review = await client.submitRevision({
1250
1280
  id: args.id,
1251
1281
  parent_revision: args.parent_revision,
@@ -1257,6 +1287,7 @@ const submitRevision = defineTool({
1257
1287
  attachments: args.attachments,
1258
1288
  built_at: args.built_at,
1259
1289
  rules_version_seen: args.rules_version_seen,
1290
+ composition_token: compositionToken,
1260
1291
  client_id: args.client_id,
1261
1292
  });
1262
1293
  return ok(`Revised.\n${renderReview(review)}`, review);
@@ -1556,6 +1587,11 @@ const getRules = defineTool({
1556
1587
  return ok(`${res.items.length} rule(s), highest precedence first.\n\n${text}`, {
1557
1588
  items: res.items,
1558
1589
  total: res.total,
1590
+ house_style_version: res.house_style_version,
1591
+ category_rules_version: res.category_rules_version,
1592
+ rule_high_water: res.rule_high_water,
1593
+ composition_token: res.composition_token,
1594
+ composition_token_expires_at: res.composition_token_expires_at,
1559
1595
  });
1560
1596
  },
1561
1597
  });
@@ -1570,6 +1606,7 @@ const saveRule = defineTool({
1570
1606
  "lineage, the prior superseded). Use this AFTER you judge a diff/comment is a generalizable rule (the judgment is " +
1571
1607
  "yours; we never run an LLM). Returns the new active rule (with its rule_layer/org_id/project_id).",
1572
1608
  inputSchema: {
1609
+ client_id: z.string().min(1).max(128).optional().describe("Stable retry id for this exact rule save; reuse it after a timeout. The MCP derives one when omitted."),
1573
1610
  rule_text: z.string().min(1).describe("The rule body, e.g. 'no em-dashes' or 'be more pushy, we need MRR'."),
1574
1611
  category_id: z.string().optional().describe("Category id (cat_…). Empty = house-style/general (D2)."),
1575
1612
  scope: z.enum(RULE_SCOPES).optional().describe("Defaults from category_id (general iff empty)."),
@@ -1591,9 +1628,11 @@ const saveRule = defineTool({
1591
1628
  .optional()
1592
1629
  .describe("Override the propagate batch (0 = base 3, bounded by rework_batch_max). Never fans one nudge to the whole queue."),
1593
1630
  },
1594
- annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
1631
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
1595
1632
  handler: async (args, { client }) => {
1633
+ const derivedClientID = `rule:${createHash("sha256").update(JSON.stringify(args)).digest("hex")}`;
1596
1634
  const rule = await client.saveRule({
1635
+ client_id: args.client_id ?? derivedClientID,
1597
1636
  rule_text: args.rule_text,
1598
1637
  category_id: args.category_id,
1599
1638
  scope: args.scope,