@theronap/agnoclast-mcp 0.9.206 → 0.9.208
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/lib/server.mjs +52 -35
- package/package.json +1 -1
package/lib/server.mjs
CHANGED
|
@@ -1550,11 +1550,11 @@ function renderNudge(payload) {
|
|
|
1550
1550
|
server.registerTool(
|
|
1551
1551
|
'search_spam_email',
|
|
1552
1552
|
{
|
|
1553
|
-
title: 'Search Gmail
|
|
1554
|
-
description: 'Search a connected
|
|
1553
|
+
title: 'Search Spam (Gmail) or Junk (Outlook) intentionally',
|
|
1554
|
+
description: 'Search a connected mailbox\'s Spam folder (Gmail) or Junk folder (Outlook) only when the user explicitly asks you to find a message that may have been marked as spam. This is read-only: results are returned for this request only and are NOT added to Agnoclast records, timeline, or future context. Give a specific search. Gmail takes its own operators (a sender, subject words, or a date); Outlook matches plain words or an address as ONE phrase, so Gmail operators such as from: do not work there. Normal mail sync never reads Spam or Junk.',
|
|
1555
1555
|
inputSchema: {
|
|
1556
|
-
query: z.string().min(2).describe('specific
|
|
1557
|
-
account: z.string().email().optional().describe('which connected Gmail
|
|
1556
|
+
query: z.string().min(2).describe('specific search within Spam/Junk. Gmail: e.g. `from:billing@example.com`, `subject:(appointment reminder)`, or `after:2026/08/01`. Outlook: plain words or an address, matched as one phrase, e.g. `billing@example.com`'),
|
|
1557
|
+
account: z.string().email().optional().describe('which connected mailbox (Gmail or Outlook) to search when more than one is available'),
|
|
1558
1558
|
limit: z.number().int().min(1).max(10).optional().describe('maximum matches to return (default 5; max 10)'),
|
|
1559
1559
|
},
|
|
1560
1560
|
},
|
|
@@ -1567,12 +1567,12 @@ function renderNudge(payload) {
|
|
|
1567
1567
|
const body = await res.json().catch(() => null)
|
|
1568
1568
|
if (!res.ok) {
|
|
1569
1569
|
if (res.status === 409 && Array.isArray(body?.accounts)) {
|
|
1570
|
-
return toolError(`Choose a connected
|
|
1570
|
+
return toolError(`Choose a connected mailbox and call again with account: ${body.accounts.join(', ')}`)
|
|
1571
1571
|
}
|
|
1572
1572
|
return toolError(body?.error ?? `Spam search failed (${res.status}).`)
|
|
1573
1573
|
}
|
|
1574
1574
|
const rows = Array.isArray(body?.messages) ? body.messages : []
|
|
1575
|
-
if (!rows.length) return { content: [{ type: 'text', text: `No Spam matches in ${body?.account ?? 'the connected
|
|
1575
|
+
if (!rows.length) return { content: [{ type: 'text', text: `No Spam/Junk matches in ${body?.account ?? 'the connected mailbox'}. Nothing was saved to Agnoclast.` }] }
|
|
1576
1576
|
const lines = [`Spam matches in ${body.account} (read-only; not saved to Agnoclast):`]
|
|
1577
1577
|
for (const m of rows) {
|
|
1578
1578
|
lines.push(`- ${m.date || 'unknown date'} · ${m.from || 'unknown sender'} · ${m.subject}\n ${m.snippet || '(no preview)'}\n ${m.sourceUri}`)
|
|
@@ -1599,15 +1599,15 @@ function renderNudge(payload) {
|
|
|
1599
1599
|
// throw in a later chunk discards the results already collected — so a 5xx can follow real changes. V4
|
|
1600
1600
|
// fixed exactly this lie in the web UI; the wording below is what keeps it fixed here.
|
|
1601
1601
|
|
|
1602
|
-
/** ONE place a
|
|
1602
|
+
/** ONE place a mailbox write failure (Gmail or Outlook) becomes agent-readable text, so three tools cannot drift on it. */
|
|
1603
1603
|
const gmailWriteError = (res, body, tool) => {
|
|
1604
1604
|
const msg = typeof body?.error === 'string' ? body.error : null
|
|
1605
1605
|
if (res.status === 409 && Array.isArray(body?.accounts) && body.accounts.length) {
|
|
1606
|
-
return toolError(`More than one connected
|
|
1606
|
+
return toolError(`More than one connected mailbox. Call ${tool} again with account: ${body.accounts.join(', ')}`)
|
|
1607
1607
|
}
|
|
1608
1608
|
if (res.status === 401 || res.status === 403) {
|
|
1609
1609
|
return toolError(
|
|
1610
|
-
`${msg ?? '
|
|
1610
|
+
`${msg ?? 'The mailbox refused that write.'} Write access may not be granted on this mailbox yet — ` +
|
|
1611
1611
|
`the person can check Connectors in Agnoclast. Nothing was changed.`)
|
|
1612
1612
|
}
|
|
1613
1613
|
if (res.status >= 500) {
|
|
@@ -1621,21 +1621,21 @@ function renderNudge(payload) {
|
|
|
1621
1621
|
server.registerTool(
|
|
1622
1622
|
'gmail_modify',
|
|
1623
1623
|
{
|
|
1624
|
-
title: 'Change labels on the user\'s own Gmail
|
|
1624
|
+
title: 'Change labels on the user\'s own email (Gmail or Outlook)',
|
|
1625
1625
|
description:
|
|
1626
|
-
'Mark read or unread, archive, move back to the inbox, or add/remove a label on messages in the USER\'S OWN connected Gmail. Only the mailbox owner can act on a message — being able to read an email record in a shared brain does NOT authorize changing it, and the server enforces that. Identify messages with recordIds (Agnoclast email record ids) or messageIds (
|
|
1626
|
+
'Mark read or unread, archive, move back to the inbox, or add/remove a label on messages in the USER\'S OWN connected mailbox, Gmail or Outlook. Only the mailbox owner can act on a message — being able to read an email record in a shared brain does NOT authorize changing it, and the server enforces that. Identify messages with recordIds (Agnoclast email record ids) or messageIds (the mailbox\'s own message ids); they must all be in ONE mailbox. For add_label/remove_label pass labelId: STARRED or IMPORTANT work in both (in Outlook they are the follow-up flag and high importance), or one of the user\'s own labels, which is Label_… in Gmail and category:<name> (an Outlook category) in Outlook. Call without labelId to get the list. THIS CHANGES THE USER\'S MAILBOX: confirm what they want before calling unless they just asked for it plainly. move_to_inbox is the undo for archive.',
|
|
1627
1627
|
inputSchema: {
|
|
1628
1628
|
action: z.enum(['mark_read', 'mark_unread', 'archive', 'move_to_inbox', 'add_label', 'remove_label'])
|
|
1629
1629
|
.describe('what to do to every message named; move_to_inbox undoes archive'),
|
|
1630
1630
|
recordIds: z.array(z.string()).optional().describe('Agnoclast email record ids, all in the same mailbox'),
|
|
1631
|
-
messageIds: z.array(z.string()).optional().describe('
|
|
1632
|
-
labelId: z.string().optional().describe('required for add_label / remove_label: one of the user\'s own labels (Label_
|
|
1633
|
-
account: z.string().email().optional().describe('which connected Gmail
|
|
1631
|
+
messageIds: z.array(z.string()).optional().describe('the mailbox\'s own message ids (Gmail or Outlook), all in the same mailbox — an alternative to recordIds'),
|
|
1632
|
+
labelId: z.string().optional().describe('required for add_label / remove_label: STARRED or IMPORTANT, or one of the user\'s own labels (Gmail Label_…, Outlook category:<name>). Omit to be shown the available labels.'),
|
|
1633
|
+
account: z.string().email().optional().describe('which connected mailbox (Gmail or Outlook), when the user has more than one'),
|
|
1634
1634
|
},
|
|
1635
1635
|
},
|
|
1636
1636
|
async ({ action, recordIds, messageIds, labelId, account }) => {
|
|
1637
1637
|
if (!recordIds?.length && !messageIds?.length) {
|
|
1638
|
-
return toolError('Name the messages to change: recordIds (Agnoclast email records) or messageIds (
|
|
1638
|
+
return toolError('Name the messages to change: recordIds (Agnoclast email records) or messageIds (the mailbox\'s message ids).')
|
|
1639
1639
|
}
|
|
1640
1640
|
// A label action with no id is not an error worth bouncing — answer it with the list, so the agent
|
|
1641
1641
|
// can act on the reply instead of guessing an id or asking the person for one.
|
|
@@ -1680,14 +1680,14 @@ function renderNudge(payload) {
|
|
|
1680
1680
|
server.registerTool(
|
|
1681
1681
|
'draft_email_reply',
|
|
1682
1682
|
{
|
|
1683
|
-
title: 'Save a reply as a draft in the user\'s Gmail',
|
|
1683
|
+
title: 'Save a reply as a draft in the user\'s mailbox (Gmail or Outlook)',
|
|
1684
1684
|
description:
|
|
1685
|
-
'Write a reply to one of the USER\'S OWN emails and save it as a DRAFT in their Gmail, threaded under the original. NOTHING IS SENT — the user reviews and sends it themselves from Gmail, so this is the safe way to help with a reply. Identify the email by recordId (an Agnoclast email record) or messageId (
|
|
1685
|
+
'Write a reply to one of the USER\'S OWN emails and save it as a DRAFT in their mailbox (Gmail or Outlook), threaded under the original. NOTHING IS SENT — the user reviews and sends it themselves from Gmail or Outlook, so this is the safe way to help with a reply. Identify the email by recordId (an Agnoclast email record) or messageId (the mailbox\'s own message id). Only the mailbox owner can draft against a message; the server enforces that. Use request_send_email instead when the user wants it actually sent.',
|
|
1686
1686
|
inputSchema: {
|
|
1687
1687
|
recordId: z.string().optional().describe('the Agnoclast email record to reply to'),
|
|
1688
|
-
messageId: z.string().optional().describe('the
|
|
1688
|
+
messageId: z.string().optional().describe('the message id to reply to (Gmail or Outlook) — an alternative to recordId'),
|
|
1689
1689
|
text: z.string().min(1).describe('the reply body, as plain text'),
|
|
1690
|
-
account: z.string().email().optional().describe('which connected Gmail
|
|
1690
|
+
account: z.string().email().optional().describe('which connected mailbox (Gmail or Outlook), when the user has more than one'),
|
|
1691
1691
|
},
|
|
1692
1692
|
},
|
|
1693
1693
|
async ({ recordId, messageId, text, account }) => {
|
|
@@ -1700,7 +1700,7 @@ function renderNudge(payload) {
|
|
|
1700
1700
|
const body = await res.json().catch(() => null)
|
|
1701
1701
|
if (!res.ok) return gmailWriteError(res, body, 'draft_email_reply')
|
|
1702
1702
|
return { content: [{ type: 'text', text:
|
|
1703
|
-
`Draft saved in ${body.account} — NOT sent. The user reviews and sends it from
|
|
1703
|
+
`Draft saved in ${body.account} — NOT sent. The user reviews and sends it from their mailbox: ${body.openUrl}` }] }
|
|
1704
1704
|
},
|
|
1705
1705
|
)
|
|
1706
1706
|
|
|
@@ -1717,9 +1717,9 @@ function renderNudge(payload) {
|
|
|
1717
1717
|
server.registerTool(
|
|
1718
1718
|
'request_send_email',
|
|
1719
1719
|
{
|
|
1720
|
-
title: 'Ask to send an email from the user\'s Gmail',
|
|
1720
|
+
title: 'Ask to send an email from the user\'s mailbox (Gmail or Outlook)',
|
|
1721
1721
|
description:
|
|
1722
|
-
'Send an email from the USER\'S OWN connected Gmail. By default this does NOT send immediately: it creates a request that WAITS for the user to approve it in Agnoclast, and it goes out at once only if their own send rule allows it. The message is frozen when the request is made, so write the final wording. Always show the user what you are about to send and get their agreement first. Returns either an approval link to relay to them, or confirmation that it already went. ⚠ NEVER call this twice for the same email — it is not idempotent, and a retry can send twice.',
|
|
1722
|
+
'Send an email from the USER\'S OWN connected mailbox, Gmail or Outlook. By default this does NOT send immediately: it creates a request that WAITS for the user to approve it in Agnoclast, and it goes out at once only if their own send rule allows it. The message is frozen when the request is made, so write the final wording. Always show the user what you are about to send and get their agreement first. Returns either an approval link to relay to them, or confirmation that it already went. ⚠ NEVER call this twice for the same email — it is not idempotent, and a retry can send twice.',
|
|
1723
1723
|
inputSchema: {
|
|
1724
1724
|
to: z.array(z.string()).min(1).describe('recipient email addresses'),
|
|
1725
1725
|
subject: z.string().min(1).describe('the subject line'),
|
|
@@ -1727,8 +1727,8 @@ function renderNudge(payload) {
|
|
|
1727
1727
|
cc: z.array(z.string()).optional().describe('cc addresses'),
|
|
1728
1728
|
bcc: z.array(z.string()).optional().describe('bcc addresses'),
|
|
1729
1729
|
replyToRecordId: z.string().optional().describe('reply in the thread of this Agnoclast email record'),
|
|
1730
|
-
replyToMessageId: z.string().optional().describe('reply in the thread of this Gmail
|
|
1731
|
-
account: z.string().email().optional().describe('which connected Gmail
|
|
1730
|
+
replyToMessageId: z.string().optional().describe('reply in the thread of this message id (Gmail or Outlook)'),
|
|
1731
|
+
account: z.string().email().optional().describe('which connected mailbox (Gmail or Outlook) to send from, when the user has more than one'),
|
|
1732
1732
|
},
|
|
1733
1733
|
},
|
|
1734
1734
|
async ({ to, subject, text, cc, bcc, replyToRecordId, replyToMessageId, account }) => {
|
|
@@ -1757,7 +1757,7 @@ function renderNudge(payload) {
|
|
|
1757
1757
|
// PRESENCE, not `in` — the route omits the key rather than sending null (pinned by
|
|
1758
1758
|
// src/send_request_verbs_test.ts). Absent means it already sent.
|
|
1759
1759
|
const reviewUrl = body?.reviewUrl
|
|
1760
|
-
const acct = body?.request?.account ?? 'your
|
|
1760
|
+
const acct = body?.request?.account ?? 'your mailbox'
|
|
1761
1761
|
const n = body?.request?.recipientCount ?? to.length
|
|
1762
1762
|
if (reviewUrl) {
|
|
1763
1763
|
// Relayed VERBATIM — this exact sentence is the contract the create route records.
|
|
@@ -4114,7 +4114,7 @@ function renderNudge(payload) {
|
|
|
4114
4114
|
'add_to_brain',
|
|
4115
4115
|
{
|
|
4116
4116
|
title: 'Add someone to one of your brains',
|
|
4117
|
-
description: "Add a person to one of your brains, or work out what it would take. Resolves which brain, whether you may add to it, and the manager id they are placed under. WITHOUT execute:true it only reports the plan and hands back a console link — call it that way first and show the user what you are about to do. WITH execute:true it performs the add, and then `brain` is REQUIRED: an access grant must never be aimed by a shared write pointer. To undo an add later, use remove_member — it shows the person what removal would do and asks first. IF THE BRAIN PAYS FOR ITS MEMBERS' SEATS, adding someone raises its owner's monthly charge: the first execute is refused with the new total, you ASK THE USER whether to accept that charge, and only on a yes re-run with confirm_seat_total set to that number. Use when asked to invite/add someone to a brain.",
|
|
4117
|
+
description: "Add a person to one of your brains, or work out what it would take. Resolves which brain, whether you may add to it, and the manager id they are placed under. WITHOUT execute:true it only reports the plan and hands back a console link — call it that way first and show the user what you are about to do. WITH execute:true it performs the add, and then `brain` is REQUIRED: an access grant must never be aimed by a shared write pointer. The person is EMAILED an invitation link — a new person sets their own password from it, someone who already has an account joins only when they accept — and no credential is ever returned. To undo an add later, use remove_member — it shows the person what removal would do and asks first. IF THE BRAIN PAYS FOR ITS MEMBERS' SEATS, adding someone raises its owner's monthly charge: the first execute is refused with the new total, you ASK THE USER whether to accept that charge, and only on a yes re-run with confirm_seat_total set to that number. Use when asked to invite/add someone to a brain.",
|
|
4118
4118
|
inputSchema: {
|
|
4119
4119
|
email: z.string().describe("the person's email address — the login their Agnoclast account is (or will be) on"),
|
|
4120
4120
|
name: z.string().optional().describe('their full name; falls back to the email local-part'),
|
|
@@ -4247,18 +4247,35 @@ function renderNudge(payload) {
|
|
|
4247
4247
|
if (r.alreadyMember) {
|
|
4248
4248
|
return { content: [{ type: 'text', text: `${payload.email} was already a member of ${target.name} — nothing changed.` }] }
|
|
4249
4249
|
}
|
|
4250
|
-
//
|
|
4251
|
-
//
|
|
4252
|
-
|
|
4253
|
-
|
|
4254
|
-
|
|
4250
|
+
// Every add is an emailed invitation on a server from 2026-10-03 on (CASA AL1): a new person sets
|
|
4251
|
+
// their own password from the link, and an existing account joins only when it accepts. No
|
|
4252
|
+
// credential ever comes back. `invitation` is absent only on an older server — kept so this
|
|
4253
|
+
// release still reads one correctly.
|
|
4254
|
+
const inv = r.invitation
|
|
4255
|
+
const sent = inv?.emailed === true
|
|
4256
|
+
const asked = Boolean(inv) && r.existingAccount
|
|
4257
|
+
const how = !inv
|
|
4258
|
+
? (r.existingAccount
|
|
4259
|
+
? 'They already had an Agnoclast account, so they keep their current login and simply gain this brain.'
|
|
4260
|
+
: `A new account was created. Temporary password: ${r.password} — they must change it on first sign-in.`)
|
|
4261
|
+
: !sent
|
|
4262
|
+
? `The invitation was saved, but the email did not send${inv.error ? `: ${String(inv.error).trim().replace(/[.\s]*$/, '')}` : ''}. Run this again to re-send it — the new link replaces the old one.`
|
|
4263
|
+
: asked
|
|
4264
|
+
? `They already have an Agnoclast account, so they were emailed an invitation. They are NOT in ${target.name} until they accept it (the link expires in 48 hours).`
|
|
4265
|
+
: `They were emailed a link to set their own password; their account works once they do (the link expires in 48 hours).${r.resent ? ' The earlier link no longer works.' : ''}`
|
|
4255
4266
|
const page = r.userPage?.status
|
|
4256
4267
|
? `\nTheir page in ${target.name}: ${r.userPage.status}${r.userPage.error ? ` (${r.userPage.error})` : ''}.`
|
|
4257
4268
|
: ''
|
|
4258
4269
|
// What the seat sync did, in the server's words: the new monthly total, or that the seat
|
|
4259
4270
|
// could NOT be charged and the brain is not paying for them yet.
|
|
4260
4271
|
const seatLine = typeof r.seatNote === 'string' && r.seatNote ? `\n${r.seatNote}` : ''
|
|
4261
|
-
|
|
4272
|
+
const headline = asked
|
|
4273
|
+
? `Invited ${payload.name} <${payload.email}> to ${target.name} as ${payload.role}, under you — they join when they accept.`
|
|
4274
|
+
: `Added ${payload.name} <${payload.email}> to ${target.name} as ${payload.role}, placed under you.`
|
|
4275
|
+
const undo = asked
|
|
4276
|
+
? '\n\nNothing to undo yet: until they accept, they are not a member. Inviting them again replaces the link.'
|
|
4277
|
+
: '\n\nTo undo this, remove_member takes them out of this brain — it shows you what that would do and asks first.'
|
|
4278
|
+
return { content: [{ type: 'text', text: `${headline}\n${how}${page}${seatLine}${undo}` }] }
|
|
4262
4279
|
}
|
|
4263
4280
|
|
|
4264
4281
|
// ADR-0023: does this brain already have a page about them? The inviter decides — list, never pick.
|
|
@@ -4297,9 +4314,9 @@ function renderNudge(payload) {
|
|
|
4297
4314
|
` Role ${payload.role}`,
|
|
4298
4315
|
` Manager you`,
|
|
4299
4316
|
'',
|
|
4300
|
-
'
|
|
4301
|
-
'
|
|
4302
|
-
'
|
|
4317
|
+
'Either way they are emailed an invitation link: a new person sets their own password from it,',
|
|
4318
|
+
'and someone who already has an Agnoclast account joins only when they accept. The button handles',
|
|
4319
|
+
'both — you do not need to know which in advance.',
|
|
4303
4320
|
'',
|
|
4304
4321
|
`payload: ${JSON.stringify(payload)}`,
|
|
4305
4322
|
].join('\n')
|
package/package.json
CHANGED