@sendora/mcp 1.0.0 → 1.1.1

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.
@@ -4,6 +4,7 @@ import { LIVE_NOTE, SIMULATOR_NOTE, WRITES_OFF_NOTE } from './tools/server.js';
4
4
  const ABOUT = "Sendora is a transactional email service: an HTTP API, SMTP submission, the TypeScript SDK @sendora/sdk, signed webhooks and a message log with every delivery event. This server carries Sendora's documentation, and with a key, tools for that key's server or account.";
5
5
  const DOCS = `Before writing code against @sendora/sdk or the HTTP API, find the answer with search_docs and read the document with read_doc; ${INDEX_URI} lists them all, and sendora://sdk/skill holds the rules an agent must keep with the SDK.`;
6
6
  const KEYS = 'Never print or repeat a Sendora key, and never write one into a file under version control; keys belong in the environment.';
7
+ const LIVE_DASHBOARD = `A person makes and revokes this server's keys, and makes and deletes its webhooks, in the dashboard, ${KEYS_PAGE}, with changes on or off, since a new key or signing secret must not reach this conversation.`;
7
8
  const ACCOUNT_DASHBOARD = `A server and a key are made in the dashboard, ${KEYS_PAGE}, since a new key is a secret this conversation must not see; a live server is deleted there too.`;
8
9
  function serverName(connection) {
9
10
  return connection.server === undefined
@@ -29,7 +30,7 @@ export function instructionsFor(connection) {
29
30
  case 'live':
30
31
  return [
31
32
  ABOUT,
32
- `This connection holds a key of ${serverName(connection)}. Call get_server first. ${LIVE_NOTE} ${connection.writes ? 'Changes are on for this connection: confirm with the user before anything reaches a real person.' : WRITES_OFF_NOTE}`,
33
+ `This connection holds a key of ${serverName(connection)}. Call get_server first. ${LIVE_NOTE} ${connection.writes ? 'Changes are on for this connection: confirm with the user before anything reaches a real person.' : WRITES_OFF_NOTE} ${LIVE_DASHBOARD}`,
33
34
  DOCS,
34
35
  KEYS,
35
36
  ].join('\n\n');
package/dist/listing.js CHANGED
@@ -31,8 +31,11 @@ export function allowed(toolClass, kind, writes) {
31
31
  return kind === 'test' || (kind === 'account' && writes);
32
32
  }
33
33
  }
34
+ /** Whether a connection of this kind, with changes on or off, lists the tool. */
35
+ export function listedFor(tool, kind, writes) {
36
+ return reaches(tool.reach, kind) && allowed(tool.class, kind, writes);
37
+ }
34
38
  /** Whether the connection lists the tool; a tool it does not list is not registered at all. */
35
39
  export function listed(tool, connection) {
36
- const writes = connection.kind === 'none' ? false : connection.writes;
37
- return reaches(tool.reach, connection.kind) && allowed(tool.class, connection.kind, writes);
40
+ return listedFor(tool, connection.kind, connection.kind !== 'none' && connection.writes);
38
41
  }
@@ -63,6 +63,7 @@ export function broadcastRequestOf(input) {
63
63
  /** `send_broadcast` (docs/design/mcp.md §4.2): a broadcast of at most 100 messages, on a test server. */
64
64
  export const sendBroadcastTool = {
65
65
  name: 'send_broadcast',
66
+ title: 'Send a test broadcast',
66
67
  class: 'irreversible',
67
68
  reach: 'server',
68
69
  operations: ['POST /v1/broadcasts'],
@@ -96,6 +97,7 @@ const broadcastIdInput = z.object({
96
97
  /** `get_broadcast` (docs/design/mcp.md §4.2): a broadcast and how far it has come. */
97
98
  export const getBroadcastTool = {
98
99
  name: 'get_broadcast',
100
+ title: 'Read a broadcast',
99
101
  class: 'read',
100
102
  reach: 'server',
101
103
  operations: ['GET /v1/broadcasts/{id}'],
@@ -121,6 +123,7 @@ export const getBroadcastTool = {
121
123
  /** `cancel_broadcast` (docs/design/mcp.md §4.2): what has not left, stopped, on a test server. */
122
124
  export const cancelBroadcastTool = {
123
125
  name: 'cancel_broadcast',
126
+ title: 'Cancel a broadcast',
124
127
  class: 'irreversible',
125
128
  reach: 'server',
126
129
  operations: ['POST /v1/broadcasts/{id}/cancel'],
@@ -11,6 +11,7 @@ const hitSchema = z.object({
11
11
  export const docsTools = [
12
12
  {
13
13
  name: 'search_docs',
14
+ title: 'Search the Sendora docs',
14
15
  class: 'docs',
15
16
  reach: 'all',
16
17
  operations: [],
@@ -45,6 +46,7 @@ export const docsTools = [
45
46
  },
46
47
  {
47
48
  name: 'read_doc',
49
+ title: 'Read a Sendora document',
48
50
  class: 'docs',
49
51
  reach: 'all',
50
52
  operations: [],
@@ -46,6 +46,7 @@ export const checkedDomainOutput = domainOutput.extend({
46
46
  /** `list_domains` (docs/design/mcp.md §4.2): the account's sending domains and their records. */
47
47
  export const listDomainsTool = {
48
48
  name: 'list_domains',
49
+ title: 'List the sending domains',
49
50
  class: 'read',
50
51
  reach: 'account',
51
52
  operations: ['GET /v1/domains'],
@@ -73,6 +74,7 @@ export const listDomainsTool = {
73
74
  /** `check_domain` (docs/design/mcp.md §4.2): the records looked up now, which changes only what Sendora knows of them. */
74
75
  export const checkDomainTool = {
75
76
  name: 'check_domain',
77
+ title: 'Check a sending domain',
76
78
  class: 'read',
77
79
  reach: 'account',
78
80
  operations: ['POST /v1/domains/{id}/verify'],
@@ -100,6 +102,7 @@ export const checkDomainTool = {
100
102
  /** `add_domain` (docs/design/mcp.md §4.2): a sending domain added with its DKIM key, and the records to publish. */
101
103
  export const addDomainTool = {
102
104
  name: 'add_domain',
105
+ title: 'Add a sending domain',
103
106
  class: 'write',
104
107
  reach: 'account',
105
108
  operations: ['POST /v1/domains'],
@@ -30,6 +30,7 @@ export const checkedInboundDomainOutput = inboundDomainOutput.extend({
30
30
  /** `list_inbound_domains` (docs/design/mcp.md §4.2): the live server's domains for receiving and their records. */
31
31
  export const listInboundDomainsTool = {
32
32
  name: 'list_inbound_domains',
33
+ title: 'List the domains for receiving',
33
34
  class: 'read',
34
35
  reach: 'live',
35
36
  operations: ['GET /v1/inbound/domains'],
@@ -57,6 +58,7 @@ export const listInboundDomainsTool = {
57
58
  /** `check_inbound_domain` (docs/design/mcp.md §4.2): the records looked up now, which changes only what Sendora knows of them. */
58
59
  export const checkInboundDomainTool = {
59
60
  name: 'check_inbound_domain',
61
+ title: 'Check a domain for receiving',
60
62
  class: 'read',
61
63
  reach: 'live',
62
64
  operations: ['POST /v1/inbound/domains/{id}/verify'],
@@ -84,6 +86,7 @@ export const checkInboundDomainTool = {
84
86
  /** `add_inbound_domain` (docs/design/mcp.md §4.2): a domain claimed for an inbound stream, and the records to add. */
85
87
  export const addInboundDomainTool = {
86
88
  name: 'add_inbound_domain',
89
+ title: 'Add a domain for receiving',
87
90
  class: 'write',
88
91
  reach: 'live',
89
92
  operations: ['POST /v1/inbound/domains'],
@@ -202,6 +202,7 @@ export const searchInboundInput = z.object({
202
202
  /** `search_inbound` (docs/design/mcp.md §5): received mail without anything its sender wrote but the From address. */
203
203
  export const searchInboundTool = {
204
204
  name: 'search_inbound',
205
+ title: 'Search received mail',
205
206
  class: 'read',
206
207
  reach: 'live',
207
208
  operations: ['POST /v1/inbound/search'],
@@ -234,6 +235,7 @@ export const searchInboundTool = {
234
235
  /** `read_inbound_message` (docs/design/mcp.md §5): one received message, what its sender wrote inside the fence. */
235
236
  export const readInboundMessageTool = {
236
237
  name: 'read_inbound_message',
238
+ title: 'Read a received message',
237
239
  class: 'read',
238
240
  reach: 'live',
239
241
  operations: ['GET /v1/inbound/{id}'],
@@ -22,6 +22,7 @@ export const createdKeyOutput = keyOutput.extend({
22
22
  /** `list_keys` (docs/design/mcp.md §4.2, §3.4): the server's keys, the connection's own marked. */
23
23
  export const listKeysTool = {
24
24
  name: 'list_keys',
25
+ title: "List the server's keys",
25
26
  class: 'read',
26
27
  reach: 'server',
27
28
  operations: ['GET /v1/tokens'],
@@ -53,6 +54,7 @@ export const listKeysTool = {
53
54
  /** `create_key` (docs/design/mcp.md §4.2): a key for a test server, answered once. */
54
55
  export const createKeyTool = {
55
56
  name: 'create_key',
57
+ title: 'Create a key',
56
58
  class: 'secret',
57
59
  reach: 'server',
58
60
  operations: ['POST /v1/tokens'],
@@ -98,6 +100,7 @@ function refusedOwnKey() {
98
100
  /** `revoke_key` (docs/design/mcp.md §4.2, §3.4): a test server's key revoked, never the connection's own. */
99
101
  export const revokeKeyTool = {
100
102
  name: 'revoke_key',
103
+ title: 'Revoke a key',
101
104
  class: 'irreversible',
102
105
  reach: 'server',
103
106
  operations: ['GET /v1/tokens', 'DELETE /v1/tokens/{id}'],
@@ -88,6 +88,7 @@ export const searchInput = z.object({
88
88
  /** `search_messages` (docs/design/mcp.md §4.2): the server's log, filtered, newest first. */
89
89
  export const searchMessagesTool = {
90
90
  name: 'search_messages',
91
+ title: 'Search the message log',
91
92
  class: 'read',
92
93
  reach: 'server',
93
94
  operations: ['POST /v1/messages/search'],
@@ -119,6 +120,7 @@ export const searchMessagesTool = {
119
120
  /** `get_message` (docs/design/mcp.md §4.2): one message, its recipients, attachments described and timeline. */
120
121
  export const getMessageTool = {
121
122
  name: 'get_message',
123
+ title: 'Read one message and its timeline',
122
124
  class: 'read',
123
125
  reach: 'server',
124
126
  operations: ['GET /v1/messages/{id}'],
@@ -98,6 +98,7 @@ const LIVE_DESCRIPTION = "Sends one email from this live server, to real people,
98
98
  /** `send_email` (docs/design/mcp.md §4.2): one message, and its outcome when it comes within the wait. */
99
99
  export const sendEmailTool = {
100
100
  name: 'send_email',
101
+ title: 'Send one email',
101
102
  class: 'write',
102
103
  reach: 'server',
103
104
  operations: ['POST /v1/email', 'GET /v1/messages/{id}'],
@@ -51,12 +51,22 @@ export const serverOutput = z.object({
51
51
  .describe('test for a server that delivers nothing, live for one that reaches real people.'),
52
52
  createdAt: z.string().describe('When it was created.'),
53
53
  });
54
+ export const sendableDomainOutput = z.object({
55
+ domain: z.string().describe('The domain, as the part of a from address after the @.'),
56
+ failingSince: z
57
+ .string()
58
+ .nullable()
59
+ .describe('When a check first found one of its records missing; mail keeps going out for 24 hours from then. Null while every record is in place.'),
60
+ });
54
61
  const outputSchema = z.object({
55
62
  server: serverOutput.describe('The server this key belongs to, and whether it is a test server.'),
56
63
  limits: z
57
64
  .array(limitOutput)
58
65
  .describe('Every limit a send on the server is held to, with its use this minute or month.'),
59
66
  streams: z.array(streamOutput).describe("The server's streams; a send names one as streamId."),
67
+ sendingDomains: z
68
+ .array(sendableDomainOutput)
69
+ .describe("The account's sending domains a from address may use now; a send from any other domain is refused with from_domain_not_verified."),
60
70
  connection: z
61
71
  .object({
62
72
  kind: z.enum(['test', 'live']).describe("The kind of this connection's key."),
@@ -77,12 +87,13 @@ export function serverConnectionOf(connection) {
77
87
  /** `get_server` (docs/design/mcp.md §4.2): where the agent stands before it acts. */
78
88
  export const getServerTool = {
79
89
  name: 'get_server',
90
+ title: 'Read the server this key belongs to',
80
91
  class: 'read',
81
92
  reach: 'server',
82
93
  operations: ['GET /v1/server', 'GET /v1/streams'],
83
94
  register: (server, { connection }) => server.registerTool('get_server', {
84
95
  title: 'Read the server this key belongs to',
85
- description: "Call this first. Answers the Sendora server this connection's key belongs to: whether it is a test server, where nothing is delivered, or a live one; every limit a send on it is held to, with this minute's and month's use; its streams; and whether this connection may change things.",
96
+ description: "Call this first. Answers the Sendora server this connection's key belongs to: whether it is a test server, where nothing is delivered, or a live one; every limit a send on it is held to, with this minute's and month's use; its streams; the domains a from address may use; and whether this connection may change things.",
86
97
  inputSchema: z.object({}),
87
98
  outputSchema,
88
99
  annotations: READING,
@@ -103,6 +114,7 @@ export const getServerTool = {
103
114
  },
104
115
  limits: own.limits,
105
116
  streams,
117
+ sendingDomains: own.sendingDomains,
106
118
  connection: { kind: own.mode, changes },
107
119
  notes: own.mode === 'test'
108
120
  ? [SIMULATOR_NOTE]
@@ -12,6 +12,7 @@ export function accountConnectionOf(connection) {
12
12
  /** `list_servers` (docs/design/mcp.md §4.2): the account's servers, each with its mode. */
13
13
  export const listServersTool = {
14
14
  name: 'list_servers',
15
+ title: "List the account's servers",
15
16
  class: 'read',
16
17
  reach: 'account',
17
18
  operations: ['GET /v1/servers'],
@@ -51,6 +52,7 @@ function refusedLiveServer() {
51
52
  */
52
53
  export const deleteTestServerTool = {
53
54
  name: 'delete_test_server',
55
+ title: 'Delete a test server',
54
56
  class: 'irreversible',
55
57
  reach: 'account',
56
58
  operations: ['GET /v1/servers/{id}', 'DELETE /v1/servers/{id}'],
@@ -4,6 +4,7 @@ import { serverConnectionOf, streamOutput } from './server.js';
4
4
  /** `create_stream` (docs/design/mcp.md §4.2): transactional or broadcast, and inbound only on a live server. */
5
5
  export const createStreamTool = {
6
6
  name: 'create_stream',
7
+ title: 'Add a stream',
7
8
  class: 'write',
8
9
  reach: 'server',
9
10
  operations: ['POST /v1/streams'],
@@ -50,6 +51,7 @@ export const createStreamTool = {
50
51
  /** `archive_stream` (docs/design/mcp.md §4.2): a test server's stream archived, for good. */
51
52
  export const archiveStreamTool = {
52
53
  name: 'archive_stream',
54
+ title: 'Archive a stream',
53
55
  class: 'irreversible',
54
56
  reach: 'server',
55
57
  operations: ['POST /v1/streams/{id}/archive'],
@@ -22,6 +22,7 @@ const STREAM = 'The stream whose list is meant, from get_server; the default tra
22
22
  /** `list_suppressions` (docs/design/mcp.md §4.2): a stream's suppression list, per stream. */
23
23
  export const listSuppressionsTool = {
24
24
  name: 'list_suppressions',
25
+ title: "List a stream's suppression list",
25
26
  class: 'read',
26
27
  reach: 'server',
27
28
  operations: ['GET /v1/suppressions'],
@@ -64,6 +65,7 @@ const REMOVE_DESCRIPTION = "Lifts an address from one stream's suppression list
64
65
  /** `remove_suppression` (docs/design/mcp.md §4.2): a hard bounce or a manual entry lifted. */
65
66
  export const removeSuppressionTool = {
66
67
  name: 'remove_suppression',
68
+ title: 'Lift a suppression',
67
69
  class: 'write',
68
70
  reach: 'server',
69
71
  operations: ['POST /v1/suppressions/delete'],
@@ -84,6 +84,7 @@ export const createWebhookInput = z.object({
84
84
  /** `list_webhooks` (docs/design/mcp.md §4.2): the server's webhooks, their secrets by id and age alone. */
85
85
  export const listWebhooksTool = {
86
86
  name: 'list_webhooks',
87
+ title: "List the server's webhooks",
87
88
  class: 'read',
88
89
  reach: 'server',
89
90
  operations: ['GET /v1/webhooks'],
@@ -111,6 +112,7 @@ export const listWebhooksTool = {
111
112
  /** `list_webhook_deliveries` (docs/design/mcp.md §4.2): one webhook's deliveries, by status. */
112
113
  export const listWebhookDeliveriesTool = {
113
114
  name: 'list_webhook_deliveries',
115
+ title: "List a webhook's deliveries",
114
116
  class: 'read',
115
117
  reach: 'server',
116
118
  operations: ['GET /v1/webhooks/{id}/deliveries'],
@@ -156,12 +158,13 @@ export const listWebhookDeliveriesTool = {
156
158
  /** `replay_webhook_delivery` (docs/design/mcp.md §4.2): an event queued for its webhook again. */
157
159
  export const replayWebhookDeliveryTool = {
158
160
  name: 'replay_webhook_delivery',
161
+ title: 'Replay a webhook delivery',
159
162
  class: 'write',
160
163
  reach: 'server',
161
164
  operations: ['POST /v1/webhooks/{id}/deliveries/{deliveryId}/replay'],
162
165
  register: (server, { connection }) => server.registerTool('replay_webhook_delivery', {
163
166
  title: 'Replay a webhook delivery',
164
- description: 'Queues one delivery of a webhook again, whatever its state, with the same payload, a fresh signature and a fresh count of attempts: for an endpoint that was down or was fixed. Each replay posts the event once more.',
167
+ description: 'Queues one delivery of a webhook again, whatever its state, with the same payload and a fresh signature: for an endpoint that was down or was fixed. Its count of attempts carries on, so a delivery that had used every retry is dead again after one more failure. Each replay posts the event once more.',
165
168
  inputSchema: z.object({
166
169
  webhookId,
167
170
  deliveryId: z.uuid().describe('The delivery, from list_webhook_deliveries.'),
@@ -189,6 +192,7 @@ export const replayWebhookDeliveryTool = {
189
192
  /** `create_webhook` (docs/design/mcp.md §4.2): a webhook on a test server, its secret answered once. */
190
193
  export const createWebhookTool = {
191
194
  name: 'create_webhook',
195
+ title: 'Add a webhook',
192
196
  class: 'secret',
193
197
  reach: 'server',
194
198
  operations: ['POST /v1/webhooks'],
@@ -219,6 +223,7 @@ export const createWebhookTool = {
219
223
  /** `delete_webhook` (docs/design/mcp.md §4.2): a test server's webhook removed, for good. */
220
224
  export const deleteWebhookTool = {
221
225
  name: 'delete_webhook',
226
+ title: 'Delete a webhook',
222
227
  class: 'irreversible',
223
228
  reach: 'server',
224
229
  operations: ['DELETE /v1/webhooks/{id}'],
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.0.0';
2
+ export const MCP_VERSION = '1.1.1';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sendora/mcp",
3
- "version": "1.0.0",
3
+ "version": "1.1.1",
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": "^3.6.1"
28
+ "@sendora/sdk": "^3.7.0"
29
29
  },
30
30
  "devDependencies": {
31
31
  "@anthropic-ai/mcpb": "2.1.2",
@@ -38,12 +38,13 @@
38
38
  "typescript": "~7.0.2",
39
39
  "vitest": "^4.1.11",
40
40
  "@sendora/db": "0.0.0",
41
- "@sendora/api": "0.0.0",
42
41
  "@sendora/domain": "0.0.0",
43
42
  "@sendora/observability": "0.0.0",
44
- "@sendora/site": "0.0.0"
43
+ "@sendora/site": "0.0.0",
44
+ "@sendora/api": "0.0.0"
45
45
  },
46
46
  "scripts": {
47
- "build": "pnpm --filter @sendora/sdk build && node scripts/build-docs.ts && node -e \"fs.rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.build.json && node -e \"fs.mkdirSync('dist/generated',{recursive:true});fs.copyFileSync('src/generated/docs.json','dist/generated/docs.json')\""
47
+ "build": "pnpm --filter @sendora/sdk build && node scripts/build-docs.ts && node -e \"fs.rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.build.json && node -e \"fs.mkdirSync('dist/generated',{recursive:true});fs.copyFileSync('src/generated/docs.json','dist/generated/docs.json')\"",
48
+ "eval": "node eval/run.ts"
48
49
  }
49
50
  }