@extrovert.dev/mcp 0.1.0-pre.4 → 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/README.md +38 -13
- package/dist/bin.d.ts +5 -7
- package/dist/bin.d.ts.map +1 -1
- package/dist/bin.js +63 -54
- package/dist/bin.js.map +1 -1
- package/dist/cli.d.ts +14 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +459 -0
- package/dist/cli.js.map +1 -0
- package/dist/client.d.ts +31 -3
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +45 -11
- package/dist/client.js.map +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.js +1 -1
- package/dist/contract.d.ts +2 -2
- package/dist/contract.js +2 -2
- package/dist/credentials.d.ts +41 -0
- package/dist/credentials.d.ts.map +1 -0
- package/dist/credentials.js +203 -0
- package/dist/credentials.js.map +1 -0
- package/dist/fixtures.d.ts +2 -2
- package/dist/fixtures.d.ts.map +1 -1
- package/dist/fixtures.js +33 -4
- package/dist/fixtures.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +7 -4
- package/dist/server.js.map +1 -1
- package/dist/stdio.d.ts.map +1 -1
- package/dist/stdio.js +24 -2
- package/dist/stdio.js.map +1 -1
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +69 -8
- package/dist/tools.js.map +1 -1
- package/dist/types.d.ts +27 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +3 -1
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";
|
|
@@ -436,6 +437,7 @@ const redeemEnrollment = defineTool({
|
|
|
436
437
|
agent_handle: args.agent_handle,
|
|
437
438
|
client_id: args.client_id,
|
|
438
439
|
});
|
|
440
|
+
const persistence = client.credentialPersistenceStatus();
|
|
439
441
|
const text = [
|
|
440
442
|
`Enrollment redeemed. Agent ${result.agent_id} is ready.`,
|
|
441
443
|
`agent_key (shown once): ${result.agent_key}`,
|
|
@@ -444,7 +446,7 @@ const redeemEnrollment = defineTool({
|
|
|
444
446
|
// whoami call — consistent with whoami's text.
|
|
445
447
|
`org: ${result.org_id || "(none)"} · project: ${result.project_id || "(none)"} (fixed — bound to this key)`,
|
|
446
448
|
`scopes: ${result.scopes.join(", ") || "(none)"}`,
|
|
447
|
-
|
|
449
|
+
credentialPersistenceMessage(persistence),
|
|
448
450
|
].join("\n");
|
|
449
451
|
return ok(text, result);
|
|
450
452
|
},
|
|
@@ -453,9 +455,12 @@ const signUp = defineTool({
|
|
|
453
455
|
name: "sign_up",
|
|
454
456
|
title: "Sign up for a free account",
|
|
455
457
|
description: "Grab a free Extrovert account in one call (no enrollment token needed). Provisions a tenant and a first inbox, then " +
|
|
456
|
-
"emails a one-time verification code to your human_email. Returns a LIMITED
|
|
457
|
-
"
|
|
458
|
-
"
|
|
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 " +
|
|
460
|
+
"MCP session keeps only through verify_signup; it expires with the code and is revoked after verification. " +
|
|
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.",
|
|
459
464
|
inputSchema: {
|
|
460
465
|
human_email: emailAddress.describe("Your email — receives the one-time verification code."),
|
|
461
466
|
username: z
|
|
@@ -473,7 +478,9 @@ const signUp = defineTool({
|
|
|
473
478
|
`inbox: ${res.address}`,
|
|
474
479
|
`agent_key (limited, shown once): ${res.agent_key}`,
|
|
475
480
|
`scopes: ${res.scopes.join(", ")}`,
|
|
476
|
-
`
|
|
481
|
+
`expires: ${res.otp_expires_at}`,
|
|
482
|
+
`This MCP session will use the limited key until verify_signup atomically exchanges it for the durable key. Do not store this bootstrap key for long-term use.`,
|
|
483
|
+
`After verification, verify_signup repeats the inbox and returns exact read_messages / get_message / wait_for_email calls so mailbox use can continue without raw HTTP.`,
|
|
477
484
|
].join("\n");
|
|
478
485
|
return ok(text, res);
|
|
479
486
|
},
|
|
@@ -481,22 +488,43 @@ const signUp = defineTool({
|
|
|
481
488
|
const verifySignup = defineTool({
|
|
482
489
|
name: "verify_signup",
|
|
483
490
|
title: "Verify signup code",
|
|
484
|
-
description: "Confirm the one-time code emailed by sign_up. On success
|
|
485
|
-
"
|
|
491
|
+
description: "Confirm the one-time code emailed by sign_up. On success the bootstrap key is revoked and you receive a NEW " +
|
|
492
|
+
"full-scope agent key (create/read/send/webhooks). This MCP session switches to it automatically. The packaged " +
|
|
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.",
|
|
486
496
|
inputSchema: {
|
|
487
497
|
otp: z.string().min(4).max(12).describe("The verification code from your signup email."),
|
|
488
498
|
},
|
|
489
499
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
490
500
|
handler: async (args, { client }) => {
|
|
491
501
|
const res = await client.verify({ otp: args.otp });
|
|
502
|
+
const quick = res.mailbox_quickstart;
|
|
503
|
+
const persistence = client.credentialPersistenceStatus();
|
|
492
504
|
const text = [
|
|
493
505
|
`Verified. ${res.message}`,
|
|
494
506
|
`agent_key (full, shown once): ${res.agent_key}`,
|
|
495
507
|
`scopes: ${res.scopes.join(", ")}`,
|
|
508
|
+
credentialPersistenceMessage(persistence),
|
|
509
|
+
`mailbox ready: ${res.address}`,
|
|
510
|
+
`First call: ${quick.list_mail.tool} ${JSON.stringify(quick.list_mail.arguments)}`,
|
|
511
|
+
`Read one result: ${quick.read_message.tool} ${JSON.stringify(quick.read_message.arguments)}`,
|
|
512
|
+
`Wait for new mail: ${quick.wait_for_mail.tool} ${JSON.stringify(quick.wait_for_mail.arguments)}`,
|
|
513
|
+
`These tools return readable text plus structured message fields; do not download raw responses or invoke jq for ordinary mailbox work.`,
|
|
514
|
+
`For outbound mail, use send_email or reply_email through the review workflow; read get_inbox first to see the effective review policy.`,
|
|
496
515
|
].join("\n");
|
|
497
516
|
return ok(text, res);
|
|
498
517
|
},
|
|
499
518
|
});
|
|
519
|
+
function credentialPersistenceMessage(status) {
|
|
520
|
+
if (status.persisted) {
|
|
521
|
+
return `Credential saved for future local sessions${status.location ? ` at ${status.location}` : ""}.`;
|
|
522
|
+
}
|
|
523
|
+
if (status.attempted) {
|
|
524
|
+
return `Automatic credential storage failed${status.error ? `: ${status.error}` : ""}. Store the complete returned agent_key now; it is not retrievable again.`;
|
|
525
|
+
}
|
|
526
|
+
return "This host did not provide durable credential storage. Store the complete returned agent_key now; it is not retrievable again.";
|
|
527
|
+
}
|
|
500
528
|
const whoami = defineTool({
|
|
501
529
|
name: "whoami",
|
|
502
530
|
title: "Who am I",
|
|
@@ -659,6 +687,7 @@ const exportEmailConfig = defineTool({
|
|
|
659
687
|
name: "export_email_config",
|
|
660
688
|
title: "Export email client config",
|
|
661
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. " +
|
|
662
691
|
"Direct SMTP is an explicit unreviewed delivery path: it bypasses Extrovert approval/review, suppression and " +
|
|
663
692
|
"contact-list enforcement, List-Unsubscribe injection, and Extrovert billing/accounting. API/MCP sends keep those " +
|
|
664
693
|
"controls. Returns a ready-to-use config; use format=json for raw connection fields.",
|
|
@@ -759,6 +788,7 @@ const sendEmail = defineTool({
|
|
|
759
788
|
.describe("Your confidence (0..1) in the category match. Feeds the min_confidence auto-send gate ONLY; " +
|
|
760
789
|
"the server never scores. Below the threshold (or omitted when one is set) the would-be auto-send " +
|
|
761
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."),
|
|
762
792
|
client_id: z
|
|
763
793
|
.string()
|
|
764
794
|
.min(1)
|
|
@@ -768,6 +798,9 @@ const sendEmail = defineTool({
|
|
|
768
798
|
},
|
|
769
799
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
770
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");
|
|
771
804
|
// Review Loop overload: any of mode/intent/category_id opts into the richer
|
|
772
805
|
// discriminated response. It does NOT decide whether a human sees the message —
|
|
773
806
|
// the policy does that either way; this only shapes what comes back.
|
|
@@ -791,6 +824,7 @@ const sendEmail = defineTool({
|
|
|
791
824
|
intent: args.intent,
|
|
792
825
|
category_id: args.category_id,
|
|
793
826
|
category_confidence: args.category_confidence,
|
|
827
|
+
composition_token: compositionToken,
|
|
794
828
|
client_id: args.client_id,
|
|
795
829
|
});
|
|
796
830
|
return ok(renderSubmitResult(result), result);
|
|
@@ -806,6 +840,7 @@ const sendEmail = defineTool({
|
|
|
806
840
|
reply_to: args.reply_to,
|
|
807
841
|
headers: args.headers,
|
|
808
842
|
attachments: args.attachments,
|
|
843
|
+
composition_token: compositionToken,
|
|
809
844
|
client_id: args.client_id,
|
|
810
845
|
});
|
|
811
846
|
return ok(renderSendOutcome("Sent", result), result);
|
|
@@ -856,6 +891,7 @@ const replyEmail = defineTool({
|
|
|
856
891
|
.max(1)
|
|
857
892
|
.optional()
|
|
858
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."),
|
|
859
895
|
client_id: z
|
|
860
896
|
.string()
|
|
861
897
|
.min(1)
|
|
@@ -868,6 +904,9 @@ const replyEmail = defineTool({
|
|
|
868
904
|
if (!args.thread_id && !args.message_id) {
|
|
869
905
|
throw new ExtrovertApiError("Provide thread_id or message_id to reply.", 400, "invalid_argument");
|
|
870
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");
|
|
871
910
|
// Review Loop overload: any of mode/intent/category_id opts into review.
|
|
872
911
|
if (args.mode !== undefined || args.intent !== undefined || args.category_id !== undefined) {
|
|
873
912
|
const result = await client.submitReplyForReview({
|
|
@@ -886,6 +925,7 @@ const replyEmail = defineTool({
|
|
|
886
925
|
intent: args.intent,
|
|
887
926
|
category_id: args.category_id,
|
|
888
927
|
category_confidence: args.category_confidence,
|
|
928
|
+
composition_token: compositionToken,
|
|
889
929
|
client_id: args.client_id,
|
|
890
930
|
});
|
|
891
931
|
return ok(renderSubmitResult(result), result);
|
|
@@ -902,6 +942,7 @@ const replyEmail = defineTool({
|
|
|
902
942
|
headers: args.headers,
|
|
903
943
|
reply_all: args.reply_all,
|
|
904
944
|
attachments: args.attachments,
|
|
945
|
+
composition_token: compositionToken,
|
|
905
946
|
client_id: args.client_id,
|
|
906
947
|
});
|
|
907
948
|
return ok(renderSendOutcome("Replied", res), res);
|
|
@@ -946,6 +987,7 @@ const forwardEmail = defineTool({
|
|
|
946
987
|
.max(1)
|
|
947
988
|
.optional()
|
|
948
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."),
|
|
949
991
|
client_id: z
|
|
950
992
|
.string()
|
|
951
993
|
.min(1)
|
|
@@ -955,6 +997,9 @@ const forwardEmail = defineTool({
|
|
|
955
997
|
},
|
|
956
998
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
957
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");
|
|
958
1003
|
// Same opt-in predicate as send/reply: mode/intent/category_id select the
|
|
959
1004
|
// richer discriminated response; the policy governs the routing regardless.
|
|
960
1005
|
if (args.mode !== undefined || args.intent !== undefined || args.category_id !== undefined) {
|
|
@@ -970,6 +1015,7 @@ const forwardEmail = defineTool({
|
|
|
970
1015
|
intent: args.intent,
|
|
971
1016
|
category_id: args.category_id,
|
|
972
1017
|
category_confidence: args.category_confidence,
|
|
1018
|
+
composition_token: compositionToken,
|
|
973
1019
|
client_id: args.client_id,
|
|
974
1020
|
});
|
|
975
1021
|
return ok(renderSubmitResult(result), result);
|
|
@@ -982,6 +1028,7 @@ const forwardEmail = defineTool({
|
|
|
982
1028
|
bcc: args.bcc,
|
|
983
1029
|
text: args.text,
|
|
984
1030
|
html: args.html,
|
|
1031
|
+
composition_token: compositionToken,
|
|
985
1032
|
client_id: args.client_id,
|
|
986
1033
|
});
|
|
987
1034
|
return ok(renderSendOutcome("Forwarded", res), res);
|
|
@@ -1215,6 +1262,7 @@ const submitRevision = defineTool({
|
|
|
1215
1262
|
"redraft could never restore a file the human reviewed the message with."),
|
|
1216
1263
|
built_at: z.string().optional().describe("When you built this draft (informational)."),
|
|
1217
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."),
|
|
1218
1266
|
client_id: z
|
|
1219
1267
|
.string()
|
|
1220
1268
|
.min(1)
|
|
@@ -1224,6 +1272,10 @@ const submitRevision = defineTool({
|
|
|
1224
1272
|
},
|
|
1225
1273
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
1226
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");
|
|
1227
1279
|
const review = await client.submitRevision({
|
|
1228
1280
|
id: args.id,
|
|
1229
1281
|
parent_revision: args.parent_revision,
|
|
@@ -1235,6 +1287,7 @@ const submitRevision = defineTool({
|
|
|
1235
1287
|
attachments: args.attachments,
|
|
1236
1288
|
built_at: args.built_at,
|
|
1237
1289
|
rules_version_seen: args.rules_version_seen,
|
|
1290
|
+
composition_token: compositionToken,
|
|
1238
1291
|
client_id: args.client_id,
|
|
1239
1292
|
});
|
|
1240
1293
|
return ok(`Revised.\n${renderReview(review)}`, review);
|
|
@@ -1534,6 +1587,11 @@ const getRules = defineTool({
|
|
|
1534
1587
|
return ok(`${res.items.length} rule(s), highest precedence first.\n\n${text}`, {
|
|
1535
1588
|
items: res.items,
|
|
1536
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,
|
|
1537
1595
|
});
|
|
1538
1596
|
},
|
|
1539
1597
|
});
|
|
@@ -1548,6 +1606,7 @@ const saveRule = defineTool({
|
|
|
1548
1606
|
"lineage, the prior superseded). Use this AFTER you judge a diff/comment is a generalizable rule (the judgment is " +
|
|
1549
1607
|
"yours; we never run an LLM). Returns the new active rule (with its rule_layer/org_id/project_id).",
|
|
1550
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."),
|
|
1551
1610
|
rule_text: z.string().min(1).describe("The rule body, e.g. 'no em-dashes' or 'be more pushy, we need MRR'."),
|
|
1552
1611
|
category_id: z.string().optional().describe("Category id (cat_…). Empty = house-style/general (D2)."),
|
|
1553
1612
|
scope: z.enum(RULE_SCOPES).optional().describe("Defaults from category_id (general iff empty)."),
|
|
@@ -1569,9 +1628,11 @@ const saveRule = defineTool({
|
|
|
1569
1628
|
.optional()
|
|
1570
1629
|
.describe("Override the propagate batch (0 = base 3, bounded by rework_batch_max). Never fans one nudge to the whole queue."),
|
|
1571
1630
|
},
|
|
1572
|
-
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint:
|
|
1631
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
1573
1632
|
handler: async (args, { client }) => {
|
|
1633
|
+
const derivedClientID = `rule:${createHash("sha256").update(JSON.stringify(args)).digest("hex")}`;
|
|
1574
1634
|
const rule = await client.saveRule({
|
|
1635
|
+
client_id: args.client_id ?? derivedClientID,
|
|
1575
1636
|
rule_text: args.rule_text,
|
|
1576
1637
|
category_id: args.category_id,
|
|
1577
1638
|
scope: args.scope,
|