@sendora/mcp 1.9.1 → 1.11.0

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.
@@ -97,6 +97,10 @@ time {
97
97
  line-height: var(--font-text-xs-line-height, 1.4);
98
98
  }
99
99
 
100
+ .reason {
101
+ overflow-wrap: anywhere;
102
+ }
103
+
100
104
  .badge {
101
105
  display: inline-block;
102
106
  padding: 1px 8px;
@@ -356,7 +360,7 @@ button:focus-visible {
356
360
  }
357
361
 
358
362
  // src/version.ts
359
- var MCP_VERSION = "1.9.1";
363
+ var MCP_VERSION = "1.11.0";
360
364
 
361
365
  // screens/host.ts
362
366
  var PROTOCOL_VERSION = "2026-01-26";
@@ -97,6 +97,10 @@ time {
97
97
  line-height: var(--font-text-xs-line-height, 1.4);
98
98
  }
99
99
 
100
+ .reason {
101
+ overflow-wrap: anywhere;
102
+ }
103
+
100
104
  .badge {
101
105
  display: inline-block;
102
106
  padding: 1px 8px;
@@ -356,7 +360,7 @@ button:focus-visible {
356
360
  }
357
361
 
358
362
  // src/version.ts
359
- var MCP_VERSION = "1.9.1";
363
+ var MCP_VERSION = "1.11.0";
360
364
 
361
365
  // screens/host.ts
362
366
  var PROTOCOL_VERSION = "2026-01-26";
@@ -541,7 +545,14 @@ button:focus-visible {
541
545
  AdminRebind: "Rerouted",
542
546
  DeferredInjectionRebind: "Rerouted",
543
547
  Rejection: "Refused",
544
- Delayed: "Delayed"
548
+ Delayed: "Delayed",
549
+ Open: "Opened",
550
+ Click: "Clicked"
551
+ };
552
+ var automatedWords = {
553
+ likely: "Likely a machine",
554
+ unlikely: "Likely a person",
555
+ unknown: "A person or a machine"
545
556
  };
546
557
  var SETTLED = /* @__PURE__ */ new Set(["delivered", "bounced", "expired"]);
547
558
  function isMessage(content) {
@@ -588,6 +599,11 @@ button:focus-visible {
588
599
  );
589
600
  }
590
601
  function reasonOf(event) {
602
+ if (event.tracking !== null) {
603
+ const who = automatedWords[event.tracking.automated];
604
+ const url = event.tracking.link?.url ?? null;
605
+ return url === null ? who : `${who}: ${url}`;
606
+ }
591
607
  const code = event.code === null ? "" : String(event.code);
592
608
  const details = event.details ?? "";
593
609
  const said = details.startsWith(code) ? details : `${code} ${details}`.trim();
@@ -41,6 +41,26 @@ export const broadcastOutput = z.object({
41
41
  .nullable()
42
42
  .describe('When nothing was left to send, or it was cancelled.'),
43
43
  });
44
+ export const broadcastDetailOutput = broadcastOutput.extend({
45
+ opened: z
46
+ .number()
47
+ .int()
48
+ .describe("Recipients with an open, machines' included, or a click that was likely a person's."),
49
+ openedByPeople: z.number().int().describe('Those of them likely to be people.'),
50
+ clicked: z.number().int().describe("Recipients who clicked a link, machines' included."),
51
+ clickedByPeople: z.number().int().describe('Those of them likely to be people.'),
52
+ links: z
53
+ .array(z.object({
54
+ index: z.number().int().describe("The link's position in the HTML, counting from 0."),
55
+ url: z
56
+ .string()
57
+ .nullable()
58
+ .describe('Where the first click led, since a substitution can give a link another URL per recipient; null once it has gone with the content.'),
59
+ clicked: z.number().int().describe("Recipients who clicked this link, machines' included."),
60
+ clickedByPeople: z.number().int().describe('Those of them likely to be people.'),
61
+ }))
62
+ .describe('Each tracked link that has a click, by its index, counted from the clicks each recipient keeps.'),
63
+ });
44
64
  /** The SDK's request for the tool's input. */
45
65
  export function broadcastRequestOf(input) {
46
66
  const fields = {
@@ -104,9 +124,9 @@ export const getBroadcastTool = {
104
124
  operations: ['GET /v1/broadcasts/{id}'],
105
125
  register: (server, { connection, targets }) => registerServerTool(server, targets, 'get_broadcast', {
106
126
  title: 'Read a broadcast',
107
- description: 'Reads a broadcast of this server and how far it has come: released and failed move as its messages leave, and status is completed once none is left. Each of its messages is in the log; search_messages with its broadcastId finds them.',
127
+ description: 'Reads a broadcast of this server and how far it has come: released and failed move as its messages leave, and status is completed once none is left. Where its mail is tracked, it also counts the recipients who opened and clicked, and those likely to be people, in all and per link. Each of its messages is in the log; search_messages with its broadcastId finds them.',
108
128
  inputSchema: broadcastIdInput,
109
- outputSchema: broadcastOutput,
129
+ outputSchema: broadcastDetailOutput,
110
130
  annotations: READING,
111
131
  }, async ({ broadcastId }, { sendora }) => {
112
132
  try {
@@ -25,6 +25,19 @@ export const messageOutput = z.object({
25
25
  address: z.string().describe('The recipient.'),
26
26
  kind: z.enum(['to', 'cc', 'bcc']).describe('Whether it was in to, cc or bcc.'),
27
27
  status: recipientStatus.describe('queued until the receiver answers; delivered, bounced or expired once it has settled; deferred while it is retried.'),
28
+ tracked: z
29
+ .boolean()
30
+ .describe('Whether tracking applied to its copy when it left. It does not say to opens, links or both, or that the HTML held a link to track.'),
31
+ opens: z.number().int().describe("Opens recorded, machines' included."),
32
+ clicks: z.number().int().describe("Clicks recorded, machines' included."),
33
+ firstOpenAt: z
34
+ .string()
35
+ .nullable()
36
+ .describe("The first open, or the first click that was likely a person's."),
37
+ firstHumanOpenAt: z
38
+ .string()
39
+ .nullable()
40
+ .describe('The same, counting only what was likely a person.'),
28
41
  }))
29
42
  .describe('Each recipient and its state: queued, delivered, deferred, bounced or expired.'),
30
43
  });
@@ -47,8 +60,10 @@ export const messageEventOutput = z.object({
47
60
  'DeferredInjectionRebind',
48
61
  'Rejection',
49
62
  'Delayed',
63
+ 'Open',
64
+ 'Click',
50
65
  ])
51
- .describe('Reception when the mail server took it, Delivery, TransientFailure for a deferral, Bounce, OOB for a bounce report by mail, Feedback for a spam complaint, and the rest.'),
66
+ .describe('Reception when the mail server took it, Delivery, TransientFailure for a deferral, Bounce, OOB for a bounce report by mail, Feedback for a spam complaint, Open and Click for what tracking recorded, and the rest.'),
52
67
  at: z.string().describe('When it happened.'),
53
68
  code: z.number().int().nullable().describe('The SMTP status code the receiver answered.'),
54
69
  details: z.string().nullable().describe("The receiver's response text."),
@@ -56,8 +71,83 @@ export const messageEventOutput = z.object({
56
71
  .string()
57
72
  .nullable()
58
73
  .describe('How a bounce was classified, such as InvalidRecipient.'),
74
+ tracking: z
75
+ .object({
76
+ automated: z
77
+ .enum(['likely', 'unlikely', 'unknown'])
78
+ .describe("Whether a machine rather than a person did it: unknown where nobody can tell, as with Apple's privacy protection."),
79
+ agent: z
80
+ .enum([
81
+ 'apple_privacy',
82
+ 'gmail_prefetch',
83
+ 'gmail_proxy',
84
+ 'yahoo_proxy',
85
+ 'outlook_prefetch',
86
+ 'proton_proxy',
87
+ 'scanner',
88
+ 'crawler',
89
+ ])
90
+ .nullable()
91
+ .describe('The proxy or machine it came through, where Sendora knows it.'),
92
+ reasons: z
93
+ .array(z.enum([
94
+ 'privacy_proxy',
95
+ 'prefetch_signature',
96
+ 'delivery_timing',
97
+ 'link_burst',
98
+ 'scanner_network',
99
+ 'bot_user_agent',
100
+ 'many_networks',
101
+ ]))
102
+ .describe("Why it was labelled a machine's."),
103
+ client: z
104
+ .enum([
105
+ 'apple_mail',
106
+ 'gmail',
107
+ 'outlook',
108
+ 'yahoo',
109
+ 'thunderbird',
110
+ 'proton',
111
+ 'browser',
112
+ 'other',
113
+ ])
114
+ .nullable()
115
+ .describe('The mail program, where Sendora knows it.'),
116
+ device: z
117
+ .enum(['desktop', 'mobile', 'tablet'])
118
+ .nullable()
119
+ .describe('The kind of device, where Sendora knows it.'),
120
+ first: z.boolean().describe("The recipient's first event of this kind."),
121
+ firstHuman: z
122
+ .boolean()
123
+ .describe("The recipient's first event of this kind that was likely a person's."),
124
+ link: z
125
+ .object({
126
+ index: z.number().int().describe("The link's position in the HTML, counting from 0."),
127
+ url: z
128
+ .string()
129
+ .nullable()
130
+ .describe("Where it led; null once it has gone with the message's text and HTML."),
131
+ })
132
+ .nullable()
133
+ .describe('The link clicked; null on an Open.'),
134
+ })
135
+ .nullable()
136
+ .describe('What tracking knows of an Open or a Click; null on any other event.'),
59
137
  });
60
138
  export const messageDetailOutput = messageOutput.extend({
139
+ tracking: z
140
+ .object({
141
+ recordingOpens: z
142
+ .boolean()
143
+ .describe("Whether new opens are recorded now, by the message's own setting, then its broadcast's, then its stream's at this moment. It says nothing of what a copy already sent carries; each recipient's tracked says whether its copy was tracked."),
144
+ recordingClicks: z.boolean().describe('The same for clicks on its links.'),
145
+ skipped: z
146
+ .enum(['no_tracking_domain', 'gmail_clipping'])
147
+ .nullable()
148
+ .describe('Why tracking stayed out of at least one copy: the from domain had no working tracking domain, or tracked links would have taken the HTML past where Gmail clips it.'),
149
+ })
150
+ .describe('Whether opens and clicks are recorded now, and why tracking may have stayed out.'),
61
151
  attachments: z
62
152
  .array(z.object({
63
153
  name: z.string().describe('The file name.'),
@@ -68,7 +158,7 @@ export const messageDetailOutput = messageOutput.extend({
68
158
  .describe('Each attachment described; the content is never returned.'),
69
159
  events: z
70
160
  .array(messageEventOutput)
71
- .describe("The timeline, oldest event first, with the receiver's answer: Reception, Delivery, TransientFailure, Bounce, Feedback for a complaint and more."),
161
+ .describe("The timeline, oldest event first, with the receiver's answer: Reception, Delivery, TransientFailure, Bounce, Feedback for a complaint and more, and Open and Click where the message was tracked. Opens and clicks are capped, while each recipient's opens and clicks count them all."),
72
162
  });
73
163
  export const searchInput = z.object({
74
164
  recipient: z
@@ -135,7 +225,7 @@ export const getMessageTool = {
135
225
  screen: messageTimeline,
136
226
  register: (server, { connection, targets }) => registerServerTool(server, targets, 'get_message', {
137
227
  title: 'Read one message and its timeline',
138
- description: "Reads one message of this server's log by its messageId: its recipients and their state, its attachments described but never their content, and every event on its timeline, oldest first, with the receiver's answer. The body is never returned.",
228
+ description: "Reads one message of this server's log by its messageId: its recipients and their state, its attachments described but never their content, and the events on its timeline, oldest first, with the receiver's answer and any opens and clicks. The body is never returned.",
139
229
  inputSchema: z.object({
140
230
  messageId: z.uuid().describe('The message, from send_email or search_messages.'),
141
231
  }),
@@ -8,6 +8,8 @@ const eventName = z.enum([
8
8
  'deferred',
9
9
  'spam_complaint',
10
10
  'unsubscribed',
11
+ 'opened',
12
+ 'clicked',
11
13
  'cap_warning',
12
14
  'cap_reached',
13
15
  'inbound',
@@ -73,7 +75,7 @@ export const createWebhookInput = z.object({
73
75
  .array(eventName)
74
76
  .min(1)
75
77
  .optional()
76
- .describe('The events to deliver; every event when left out.'),
78
+ .describe('The events to deliver; every event except opened and clicked when left out.'),
77
79
  streamId: z
78
80
  .uuid()
79
81
  .optional()
package/dist/version.js CHANGED
@@ -1,2 +1,2 @@
1
1
  /** This package's version, held equal to its package.json by a test. */
2
- export const MCP_VERSION = '1.9.1';
2
+ export const MCP_VERSION = '1.11.0';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sendora/mcp",
3
- "version": "1.9.1",
3
+ "version": "1.11.0",
4
4
  "description": "The official MCP server for Sendora: its documentation and API for coding agents.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -25,7 +25,7 @@
25
25
  "dependencies": {
26
26
  "@modelcontextprotocol/server": "^2.1.0",
27
27
  "zod": "^4.6.2",
28
- "@sendora/sdk": "^5.1.0"
28
+ "@sendora/sdk": "^5.3.0"
29
29
  },
30
30
  "devDependencies": {
31
31
  "@anthropic-ai/mcpb": "2.1.2",