@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.
- package/LICENSE +2 -0
- package/dist/errors.js +11 -0
- package/dist/generated/docs.json +1 -1
- package/dist/generated/screens/domain-records.html +1 -1
- package/dist/generated/screens/message-timeline.html +1 -1
- package/dist/icons.js +20 -0
- package/dist/server.js +2 -0
- package/dist/tools/message-fields.js +13 -6
- package/dist/tools/messages.js +11 -4
- package/dist/tools/server.js +12 -0
- package/dist/tools/webhooks.js +2 -0
- package/dist/version.js +1 -1
- package/package.json +4 -4
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 =
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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>.'),
|
package/dist/tools/messages.js
CHANGED
|
@@ -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
|
|
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:
|
|
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.'),
|
package/dist/tools/server.js
CHANGED
|
@@ -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 },
|
package/dist/tools/webhooks.js
CHANGED
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.
|
|
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.
|
|
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.
|
|
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/
|
|
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": {
|