@theronap/agnoclast-mcp 0.9.207 → 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.
Files changed (2) hide show
  1. package/lib/server.mjs +25 -25
  2. 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 Spam intentionally',
1554
- description: 'Search a connected Gmail Spam folder 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 Gmail search such as a sender, subject words, or date. Normal Gmail sync never reads Spam.',
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 Gmail search within Spam, e.g. `from:billing@example.com`, `subject:(appointment reminder)`, or `after:2026/08/01`'),
1557
- account: z.string().email().optional().describe('which connected Gmail account to search when more than one is available'),
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 Gmail account and call again with account: ${body.accounts.join(', ')}`)
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 Gmail account'}. Nothing was saved to Agnoclast.` }] }
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 Gmail write failure becomes agent-readable text, so three tools cannot drift on it. */
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 Gmail account. Call ${tool} again with account: ${body.accounts.join(', ')}`)
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 ?? 'Gmail refused that write.'} Gmail write access may not be granted on this account yet — ` +
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 messages',
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 (Gmail ids); they must all be in ONE mailbox. For add_label/remove_label pass labelId — one of the user\'s own labels (Label_…), or STARRED or IMPORTANT; 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.',
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('Gmail message ids, all in the same mailbox — an alternative to recordIds'),
1632
- labelId: z.string().optional().describe('required for add_label / remove_label: one of the user\'s own labels (Label_…), or STARRED or IMPORTANT. Omit to be shown the available labels.'),
1633
- account: z.string().email().optional().describe('which connected Gmail account, when the user has more than one'),
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 (Gmail ids).')
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 (a Gmail 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.',
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 Gmail message id to reply to — an alternative to recordId'),
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 account, when the user has more than one'),
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 Gmail: ${body.openUrl}` }] }
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 message id'),
1731
- account: z.string().email().optional().describe('which connected Gmail account to send from, when the user has more than one'),
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 Gmail'
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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theronap/agnoclast-mcp",
3
- "version": "0.9.207",
3
+ "version": "0.9.208",
4
4
  "description": "Connect your AI assistant to Cortex — your org's projects, activity, gaps, and directives, scoped to you.",
5
5
  "type": "module",
6
6
  "bin": {