@sendora/mcp 1.5.1 → 1.8.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.
@@ -356,7 +356,7 @@ button:focus-visible {
356
356
  }
357
357
 
358
358
  // src/version.ts
359
- var MCP_VERSION = "1.5.1";
359
+ var MCP_VERSION = "1.8.0";
360
360
 
361
361
  // screens/host.ts
362
362
  var PROTOCOL_VERSION = "2026-01-26";
@@ -356,7 +356,7 @@ button:focus-visible {
356
356
  }
357
357
 
358
358
  // src/version.ts
359
- var MCP_VERSION = "1.5.1";
359
+ var MCP_VERSION = "1.8.0";
360
360
 
361
361
  // screens/host.ts
362
362
  var PROTOCOL_VERSION = "2026-01-26";
package/dist/icons.js ADDED
@@ -0,0 +1,20 @@
1
+ /*
2
+ * A client shows a server's icon at 16 to 32 px, where brand/README.md asks
3
+ * for the small cut on its cobalt tile, which reads on light and dark alike.
4
+ * The files are written inline, so neither the npm package nor the hosted
5
+ * image reads brand/ at run time. test/brand.test.ts checks that they decode
6
+ * to brand/favicon.svg and brand/favicon-64.png.
7
+ */
8
+ /** The icons that serverInfo lists. */
9
+ export const icons = [
10
+ {
11
+ src: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2NCA2NCI+PHJlY3Qgd2lkdGg9IjY0IiBoZWlnaHQ9IjY0IiByeD0iOCIgZmlsbD0iIzIyNDVkMSIvPjxwYXRoIGZpbGw9IiNmZmZmZmYiIGQ9Ik00IDIwSDEwLjZDMTIuNyAyMCAxNC44IDIwLjggMTYuMyAyMi4zTDI5LjkgMzZINDBWNDRIMjkuNEMyNy4zIDQ0IDI1LjIgNDMuMiAyMy43IDQxLjdMMTAuMSAyOEg0Wk01NCA0NFE1MS41IDQ0IDQ5LjcgNDIuM1E0OCA0MC41IDQ4IDM4UTQ4IDM1LjUgNDkuNyAzMy43UTUxLjUgMzIgNTQgMzJRNTYuNSAzMiA1OC4zIDMzLjdRNjAgMzUuNSA2MCAzOFE2MCA0MC41IDU4LjMgNDIuM1E1Ni41IDQ0IDU0IDQ0WiIvPjwvc3ZnPgo=',
12
+ mimeType: 'image/svg+xml',
13
+ sizes: ['any'],
14
+ },
15
+ {
16
+ src: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAEP0lEQVR42u2bS0xcVRjHf+fOINAyDFAp1LS0ZXCjVAXRmlpsRdChMakLU+MjpQn4rCZCjLpQ042xCwVjqDYtNZamBqUL2yYQacWkunAhFiqlSqkQWkt5TQemM8M87hwXBFKRx6UOE4Z7/6ubOd+d5P+b75zvu+fOEUxRZmHbBgllwGMC1iFYTjRL4pbQg5CnBdT8deq+9puHxcRFlr0zNqR6q5DyZYRQWIqSqCD3xyWbKzrqs/2TALLsnbFq0NsoBI+iA0lJc3yyqbijPtuvAKiq91O9mAcQgoIxZ/ATAJFZ2LYBZOuSTftZpoMwhe5VJJTpzvz45DdJVZQqCArRqSSiSAGZgW4l1yoCkaBX+0IIi4LOZQAwABgADAAGAAOAAcAAYADQq8xTP7jUdM9Muyg4RoJ09fpo/mWUbxoduNxq9D8PZBa1SS0ApsrpUtlT/Tcnf3TqcwokWUxUvZvB68+n6XcNEALKS9J444U0fS+Cb+5Mi9pMCFsVKC9JY/dzK/VdBit2pUcdhLD3ARW70nnt2ZXRWwZn0g57Ch+Wr0YR2r744y+v8UXdwKIHYEq2vbpHS+D5Li8DwwEKHkpEaICwKScBf0Dya7t7aQCYhOAIULBRG4SHcxLw+UO0nPcsDQAA7RfnCSHXwphv8UIw38pNdQ0O4mIV3nvlDk0Q3i5dxZhfUvvd0IIZSVhmouSpFdjzraxfHYuU0HPFR8OZEWqPD+H2hsKTARNq/cODy6PySJ5FU8e4Jc/C9dEg5/70ht181to46ipt2DdbSU2JIcYsiDELUlNi2JSTwJNbk/j5NxeOETV8AABaL3hwe0Lka4XwQCLDI0F+7/SG9Zevq7SxJv22GWOsFhNb8hI59r2DQFCGDwDA2QsevGMhNt+vDcLWBxMZcoYPQtnTqdjzrXPGWS0mbnhD/6lKmvuAufTSjlTeKVsV9vS2PX5u1vGTn9/JXVnxmhfw7bsvLkwneODbQT460BfxVXz9mljNsZnTxIa1Fa45Nsjeg32R/r/P/4oN+7PAwfpBKr+6FjEAPVd8mmO7p4ldkE3RfV8PUHW4PyIAGs6MzCPWGbld4eqj/VQd7p9Xit6Kao8PcbnPP2dc71U/R04MR3ZbvPpoP+V7e3G6Fm732O0NUfZ+N71X/bOaf/GDbjzTdINhK4OzybLcxDPFKRRstGDLiGNFkllTC62lDE5oWbzCzu23U5xvnVztL/X6aPzJyZETw9OajxgA482QAcAAYAAwABgADAAGAAOAAcAAsPgASCldejUvJaMKQlzWcQL0KiBP6dW9gCZFQM34iUrdnZhSlZB6SBk/Syv3688/+7qaczsUgLhkc4VE/qAj+6dTrgfemiyDHfXZ/vgk8zaQ1Ut6OkhUKfks2RHc1tKSF/jX6fEJ2Z44e7dURalEFCHkumg/VieRN5CiR0CTElIPdTXndtw8/g9pUJA+sUHj5gAAAABJRU5ErkJggg==',
17
+ mimeType: 'image/png',
18
+ sizes: ['64x64'],
19
+ },
20
+ ];
package/dist/server.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { McpServer } from '@modelcontextprotocol/server';
2
2
  import { registerDocuments } from './documents.js';
3
+ import { icons } from './icons.js';
3
4
  import { instructionsFor } from './instructions.js';
4
5
  import { listed, targetsFor } from './listing.js';
5
6
  import { registerPrompts } from './prompts.js';
@@ -24,6 +25,7 @@ export function buildServer(docs, connection) {
24
25
  title: 'Sendora',
25
26
  version: MCP_VERSION,
26
27
  websiteUrl: 'https://sendora.se/docs/mcp',
28
+ icons,
27
29
  }, {
28
30
  instructions: instructionsFor(connection),
29
31
  cacheHints: {
@@ -13,6 +13,13 @@ export const MAX_MESSAGE_BYTES = 10 * 1024 * 1024;
13
13
  /** The tool's own bound on a message's attachments together, tighter than the API's (§4.2). */
14
14
  export const MAX_ATTACHMENT_BYTES = 5 * 1024 * 1024;
15
15
  const BASE64_LINE_LENGTH = 76;
16
+ /*
17
+ * The control characters, U+0000 to U+001F and U+007F to U+009F, which the
18
+ * domain matches as \p{Cc}. ChatGPT checks every tool's patterns with an
19
+ * engine that cannot read \p{…}, and then refuses every tool of the
20
+ * connection (docs/design/oauth.md §1.2), so the patterns spell them out.
21
+ */
22
+ export const CONTROL = String.raw `\x00-\x1F\x7F-\x9F`;
16
23
  export const address = z
17
24
  .union([
18
25
  z.email(),
@@ -30,7 +37,7 @@ export const address = z
30
37
  ])
31
38
  .describe('An address as a plain string, or an object with the address and a display name.');
32
39
  const HEADER_NAME = /^[A-Za-z0-9][A-Za-z0-9-]{0,63}$/;
33
- const HEADER_VALUE = /^[^\p{Cc}]{1,998}$/u;
40
+ const HEADER_VALUE = new RegExp(`^[^${CONTROL}]{1,998}$`, 'u');
34
41
  const DENIED_HEADERS = new Set([
35
42
  'from',
36
43
  'sender',
@@ -87,7 +94,7 @@ const METADATA_KEY = /^[A-Za-z0-9_.-]{1,40}$/;
87
94
  export const metadata = z
88
95
  .record(z.string(), z
89
96
  .string()
90
- .regex(/^[^\p{Cc}]{0,500}$/u, 'metadata values are at most 500 characters without control characters'))
97
+ .regex(new RegExp(`^[^${CONTROL}]{0,500}$`, 'u'), 'metadata values are at most 500 characters without control characters'))
91
98
  .superRefine((values, ctx) => {
92
99
  const keys = Object.keys(values);
93
100
  if (keys.length > 20) {
@@ -107,7 +114,7 @@ const SUBSTITUTION_KEY = /^[A-Za-z0-9_]{1,40}$/;
107
114
  export const substitutions = z
108
115
  .record(z.string(), z
109
116
  .string()
110
- .regex(/^[^\p{Cc}]{0,500}$/u, 'substitution values are at most 500 characters without control characters'))
117
+ .regex(new RegExp(`^[^${CONTROL}]{0,500}$`, 'u'), 'substitution values are at most 500 characters without control characters'))
111
118
  .superRefine((values, ctx) => {
112
119
  const keys = Object.keys(values);
113
120
  if (keys.length > 20) {
@@ -133,13 +140,13 @@ export const substitutions = z
133
140
  export const tag = z
134
141
  .string()
135
142
  .trim()
136
- .regex(/^[^\p{Cc}]{1,100}$/u, 'a tag is 1 to 100 characters without control characters')
143
+ .regex(new RegExp(`^[^${CONTROL}]{1,100}$`, 'u'), 'a tag is 1 to 100 characters without control characters')
137
144
  .nullable()
138
145
  .optional();
139
146
  export const attachment = z.object({
140
147
  name: z
141
148
  .string()
142
- .regex(/^[^\p{Cc}/\\]{1,255}$/u, 'a file name without path separators, at most 255 characters')
149
+ .regex(new RegExp(String.raw `^[^${CONTROL}/\\]{1,255}$`, 'u'), 'a file name without path separators, at most 255 characters')
143
150
  .describe('The file name, without path separators.'),
144
151
  content: z
145
152
  .base64('base64 without line breaks')
@@ -152,7 +159,7 @@ export const attachment = z.object({
152
159
  .describe('A media type such as application/pdf; application/octet-stream when left out.'),
153
160
  contentId: z
154
161
  .string()
155
- .regex(/^[^\s<>\p{Cc}]{1,255}$/u, 'a content id without whitespace or angle brackets')
162
+ .regex(new RegExp(String.raw `^[^\s<>${CONTROL}]{1,255}$`, 'u'), 'a content id without whitespace or angle brackets')
156
163
  .nullable()
157
164
  .optional()
158
165
  .describe('Lets the HTML part show the attachment as cid:<contentId>.'),
@@ -71,13 +71,20 @@ export const messageDetailOutput = messageOutput.extend({
71
71
  .describe("The timeline, oldest event first, with the receiver's answer: Reception, Delivery, TransientFailure, Bounce, Feedback for a complaint and more."),
72
72
  });
73
73
  export const searchInput = z.object({
74
- recipient: z.email().optional().describe('Messages to this address, in to, cc or bcc.'),
74
+ recipient: z
75
+ .string()
76
+ .trim()
77
+ .min(3)
78
+ .max(254)
79
+ .optional()
80
+ .describe('Messages to this address, in to, cc or bcc. A whole address matches exactly. Anything else of three characters or more matches anywhere in an address, such as kommun.se.'),
75
81
  streamId: z.uuid().optional().describe('Messages on this stream of the server.'),
76
82
  broadcastId: z.uuid().optional().describe('Messages of this broadcast.'),
77
83
  tag: z.string().trim().min(1).max(100).optional().describe('Messages sent with this tag.'),
78
- status: recipientStatus
84
+ status: z
85
+ .union([recipientStatus, z.array(recipientStatus).min(1).max(5)])
79
86
  .optional()
80
- .describe('Messages with at least one recipient in this state.'),
87
+ .describe('Messages with at least one recipient in this state, or in any of these states.'),
81
88
  from: z.iso
82
89
  .datetime({ offset: true })
83
90
  .optional()
@@ -95,7 +102,7 @@ export const searchMessagesTool = {
95
102
  operations: ['POST /v1/messages/search'],
96
103
  register: (server, { connection, targets }) => registerServerTool(server, targets, 'search_messages', {
97
104
  title: 'Search the message log',
98
- description: "Searches this server's message log, newest first, by recipient, stream, broadcast, tag, a recipient's state and when the message was submitted. Answers each message with its recipients and their state, and next for the following page; get_message reads one message's timeline.",
105
+ description: "Searches this server's message log, newest first, by a recipient's address or part of one, stream, broadcast, tag, a recipient's state and when the message was submitted. Answers each message with its recipients and their state, and next for the following page; get_message reads one message's timeline.",
99
106
  inputSchema: searchInput,
100
107
  outputSchema: z.object({
101
108
  messages: z.array(messageOutput).describe('The page of messages, newest first.'),
@@ -28,6 +28,14 @@ export const limitOutput = z.object({
28
28
  used: z.number().int().describe('Emails the period has taken so far.'),
29
29
  resetsAt: z.string().describe('When the period ends and the count starts again.'),
30
30
  });
31
+ export const inboundCapOutput = z.object({
32
+ limit: z
33
+ .number()
34
+ .int()
35
+ .describe("Emails the account may receive in the calendar month in UTC, over every inbound stream; the account's monthly cap on sending until an administrator moves it."),
36
+ used: z.number().int().describe('Emails received this month so far, one per recipient.'),
37
+ resetsAt: z.string().describe('When the month ends and the count starts again.'),
38
+ });
31
39
  export const streamOutput = z.object({
32
40
  streamId: z.string().describe('What a send names as streamId.'),
33
41
  kind: z
@@ -76,6 +84,9 @@ const outputSchema = z.object({
76
84
  limits: z
77
85
  .array(limitOutput)
78
86
  .describe('Every limit a send on the server is held to, with its use this minute or month.'),
87
+ inboundCap: inboundCapOutput
88
+ .nullable()
89
+ .describe("The account's monthly cap on received mail, apart from the limits on sending, with its use; past it, mail to the account is refused for now. Null on a test server, which receives no mail."),
79
90
  streams: z.array(streamOutput).describe("The server's streams; a send names one as streamId."),
80
91
  sendingDomains: z
81
92
  .array(sendableDomainOutput)
@@ -128,6 +139,7 @@ export const getServerTool = {
128
139
  createdAt: own.createdAt,
129
140
  },
130
141
  limits: own.limits,
142
+ inboundCap: own.inboundCap,
131
143
  streams,
132
144
  sendingDomains: own.sendingDomains,
133
145
  connection: { kind: own.mode, changes },
@@ -11,6 +11,8 @@ const eventName = z.enum([
11
11
  'cap_warning',
12
12
  'cap_reached',
13
13
  'inbound',
14
+ 'inbound_cap_warning',
15
+ 'inbound_cap_reached',
14
16
  ]);
15
17
  export const webhookOutput = z.object({
16
18
  webhookId: z.string().describe('What the deliveries and a delete refer to.'),
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.5.1';
2
+ export const MCP_VERSION = '1.8.0';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sendora/mcp",
3
- "version": "1.5.1",
3
+ "version": "1.8.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": "^4.1.0"
28
+ "@sendora/sdk": "^4.4.0"
29
29
  },
30
30
  "devDependencies": {
31
31
  "@anthropic-ai/mcpb": "2.1.2",
@@ -39,10 +39,10 @@
39
39
  "fflate": "^0.8.3",
40
40
  "typescript": "~7.0.2",
41
41
  "vitest": "^4.1.11",
42
- "@sendora/api": "0.0.0",
43
42
  "@sendora/db": "0.0.0",
44
- "@sendora/domain": "0.0.0",
43
+ "@sendora/api": "0.0.0",
45
44
  "@sendora/observability": "0.0.0",
45
+ "@sendora/domain": "0.0.0",
46
46
  "@sendora/site": "0.0.0"
47
47
  },
48
48
  "scripts": {