@sendora/mcp 1.3.3 → 1.4.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/dist/errors.js +14 -8
- package/dist/generated/docs.json +1 -1
- package/dist/grant.js +95 -0
- package/dist/handler.js +137 -36
- package/dist/instructions.js +18 -2
- package/dist/listing.js +23 -0
- package/dist/server.js +2 -2
- package/dist/target.js +49 -0
- package/dist/tools/broadcasts.js +14 -13
- package/dist/tools/domains.js +3 -3
- package/dist/tools/inbound-domains.js +13 -13
- package/dist/tools/inbound.js +9 -9
- package/dist/tools/keys.js +11 -13
- package/dist/tools/messages.js +9 -9
- package/dist/tools/send.js +16 -8
- package/dist/tools/server.js +27 -14
- package/dist/tools/servers.js +29 -9
- package/dist/tools/streams.js +19 -14
- package/dist/tools/suppressions.js +39 -37
- package/dist/tools/webhooks.js +21 -21
- package/dist/version.js +1 -1
- package/package.json +4 -4
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { toolError } from '../errors.js';
|
|
3
3
|
import { READING } from '../tool.js';
|
|
4
|
-
import {
|
|
4
|
+
import { modesOf, registerServerTool } from '../target.js';
|
|
5
5
|
export const suppressionOutput = z.object({
|
|
6
6
|
streamId: z.string().describe('The stream whose list the address is on.'),
|
|
7
7
|
address: z.string().describe('The recipient, lower-cased.'),
|
|
@@ -26,7 +26,7 @@ export const listSuppressionsTool = {
|
|
|
26
26
|
class: 'read',
|
|
27
27
|
reach: 'server',
|
|
28
28
|
operations: ['GET /v1/suppressions'],
|
|
29
|
-
register: (server, { connection }) => server
|
|
29
|
+
register: (server, { connection, targets }) => registerServerTool(server, targets, 'list_suppressions', {
|
|
30
30
|
title: "List a stream's suppression list",
|
|
31
31
|
description: 'Lists the addresses one stream of this server will not send to, and why: a hard bounce, a spam complaint, an unsubscribe or a manual entry. A send to one of them is refused with recipient_suppressed; a hard bounce or a manual entry can be lifted with remove_suppression.',
|
|
32
32
|
inputSchema: z.object({
|
|
@@ -48,20 +48,27 @@ export const listSuppressionsTool = {
|
|
|
48
48
|
.describe('Pass as after for the next page; null on the last.'),
|
|
49
49
|
}),
|
|
50
50
|
annotations: READING,
|
|
51
|
-
}, async (query) => {
|
|
51
|
+
}, async (query, { sendora }) => {
|
|
52
52
|
try {
|
|
53
|
-
const page = await
|
|
53
|
+
const page = await sendora.suppressions.list(query);
|
|
54
54
|
return {
|
|
55
55
|
content: [{ type: 'text', text: JSON.stringify(page) }],
|
|
56
56
|
structuredContent: page,
|
|
57
57
|
};
|
|
58
58
|
}
|
|
59
59
|
catch (error) {
|
|
60
|
-
return toolError(error);
|
|
60
|
+
return toolError(error, connection);
|
|
61
61
|
}
|
|
62
62
|
}),
|
|
63
63
|
};
|
|
64
64
|
const REMOVE_DESCRIPTION = "Lifts an address from one stream's suppression list so this server may send to it again on that stream: a hard bounce or a manual entry. A spam complaint or an unsubscribe cannot be lifted, since the recipient asked.";
|
|
65
|
+
function removeDescription(modes) {
|
|
66
|
+
if (!modes.includes('live')) {
|
|
67
|
+
return REMOVE_DESCRIPTION;
|
|
68
|
+
}
|
|
69
|
+
const server = modes.includes('test') ? 'a live server' : 'this live server';
|
|
70
|
+
return `${REMOVE_DESCRIPTION} On ${server} the address is a real person's: confirm with the user first.`;
|
|
71
|
+
}
|
|
65
72
|
/** `remove_suppression` (docs/design/mcp.md §4.2): a hard bounce or a manual entry lifted. */
|
|
66
73
|
export const removeSuppressionTool = {
|
|
67
74
|
name: 'remove_suppression',
|
|
@@ -69,36 +76,31 @@ export const removeSuppressionTool = {
|
|
|
69
76
|
class: 'write',
|
|
70
77
|
reach: 'server',
|
|
71
78
|
operations: ['POST /v1/suppressions/delete'],
|
|
72
|
-
register: (server, { connection }) => {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
content: [{ type: 'text', text: JSON.stringify(output) }],
|
|
100
|
-
structuredContent: output,
|
|
101
|
-
};
|
|
102
|
-
});
|
|
103
|
-
},
|
|
79
|
+
register: (server, { connection, targets }) => registerServerTool(server, targets, 'remove_suppression', {
|
|
80
|
+
title: 'Lift a suppression',
|
|
81
|
+
description: removeDescription(modesOf(targets)),
|
|
82
|
+
inputSchema: z.object({
|
|
83
|
+
address: z.email().describe('The address to send to again.'),
|
|
84
|
+
streamId: z.uuid().optional().describe(STREAM),
|
|
85
|
+
}),
|
|
86
|
+
outputSchema: removedOutput,
|
|
87
|
+
annotations: {
|
|
88
|
+
readOnlyHint: false,
|
|
89
|
+
destructiveHint: false,
|
|
90
|
+
idempotentHint: true,
|
|
91
|
+
openWorldHint: false,
|
|
92
|
+
},
|
|
93
|
+
}, async ({ address, streamId }, { sendora }) => {
|
|
94
|
+
try {
|
|
95
|
+
await sendora.suppressions.delete({ address, streamId });
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
return toolError(error, connection);
|
|
99
|
+
}
|
|
100
|
+
const output = { address, streamId: streamId ?? null };
|
|
101
|
+
return {
|
|
102
|
+
content: [{ type: 'text', text: JSON.stringify(output) }],
|
|
103
|
+
structuredContent: output,
|
|
104
|
+
};
|
|
105
|
+
}),
|
|
104
106
|
};
|
package/dist/tools/webhooks.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { toolError } from '../errors.js';
|
|
3
3
|
import { READING } from '../tool.js';
|
|
4
|
-
import {
|
|
4
|
+
import { registerServerTool } from '../target.js';
|
|
5
5
|
const eventName = z.enum([
|
|
6
6
|
'delivered',
|
|
7
7
|
'bounced',
|
|
@@ -88,7 +88,7 @@ export const listWebhooksTool = {
|
|
|
88
88
|
class: 'read',
|
|
89
89
|
reach: 'server',
|
|
90
90
|
operations: ['GET /v1/webhooks'],
|
|
91
|
-
register: (server, { connection }) => server
|
|
91
|
+
register: (server, { connection, targets }) => registerServerTool(server, targets, 'list_webhooks', {
|
|
92
92
|
title: "List the server's webhooks",
|
|
93
93
|
description: "Lists this server's webhooks: where each posts, which events and stream, whether it is switched on, and its signing secrets by id and age. A secret's value is shown once, when it is made, and never answered again.",
|
|
94
94
|
inputSchema: z.object({}),
|
|
@@ -96,16 +96,16 @@ export const listWebhooksTool = {
|
|
|
96
96
|
webhooks: z.array(webhookOutput).describe("The server's webhooks."),
|
|
97
97
|
}),
|
|
98
98
|
annotations: READING,
|
|
99
|
-
}, async () => {
|
|
99
|
+
}, async (_input, { sendora }) => {
|
|
100
100
|
try {
|
|
101
|
-
const list = await
|
|
101
|
+
const list = await sendora.webhooks.list();
|
|
102
102
|
return {
|
|
103
103
|
content: [{ type: 'text', text: JSON.stringify(list) }],
|
|
104
104
|
structuredContent: list,
|
|
105
105
|
};
|
|
106
106
|
}
|
|
107
107
|
catch (error) {
|
|
108
|
-
return toolError(error);
|
|
108
|
+
return toolError(error, connection);
|
|
109
109
|
}
|
|
110
110
|
}),
|
|
111
111
|
};
|
|
@@ -116,7 +116,7 @@ export const listWebhookDeliveriesTool = {
|
|
|
116
116
|
class: 'read',
|
|
117
117
|
reach: 'server',
|
|
118
118
|
operations: ['GET /v1/webhooks/{id}/deliveries'],
|
|
119
|
-
register: (server, { connection }) => server
|
|
119
|
+
register: (server, { connection, targets }) => registerServerTool(server, targets, 'list_webhook_deliveries', {
|
|
120
120
|
title: "List a webhook's deliveries",
|
|
121
121
|
description: "Lists the events on their way to one webhook, newest first: pending with its next attempt, delivered, or dead after its retries, with the endpoint's last answer. A dead delivery can be sent again with replay_webhook_delivery.",
|
|
122
122
|
inputSchema: z.object({
|
|
@@ -142,16 +142,16 @@ export const listWebhookDeliveriesTool = {
|
|
|
142
142
|
.describe('Pass as after for the next page; null on the last.'),
|
|
143
143
|
}),
|
|
144
144
|
annotations: READING,
|
|
145
|
-
}, async ({ webhookId: id, ...query }) => {
|
|
145
|
+
}, async ({ webhookId: id, ...query }, { sendora }) => {
|
|
146
146
|
try {
|
|
147
|
-
const page = await
|
|
147
|
+
const page = await sendora.webhooks.deliveries(id, query);
|
|
148
148
|
return {
|
|
149
149
|
content: [{ type: 'text', text: JSON.stringify(page) }],
|
|
150
150
|
structuredContent: page,
|
|
151
151
|
};
|
|
152
152
|
}
|
|
153
153
|
catch (error) {
|
|
154
|
-
return toolError(error);
|
|
154
|
+
return toolError(error, connection);
|
|
155
155
|
}
|
|
156
156
|
}),
|
|
157
157
|
};
|
|
@@ -162,7 +162,7 @@ export const replayWebhookDeliveryTool = {
|
|
|
162
162
|
class: 'write',
|
|
163
163
|
reach: 'server',
|
|
164
164
|
operations: ['POST /v1/webhooks/{id}/deliveries/{deliveryId}/replay'],
|
|
165
|
-
register: (server, { connection }) => server
|
|
165
|
+
register: (server, { connection, targets }) => registerServerTool(server, targets, 'replay_webhook_delivery', {
|
|
166
166
|
title: 'Replay a webhook delivery',
|
|
167
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.',
|
|
168
168
|
inputSchema: z.object({
|
|
@@ -176,16 +176,16 @@ export const replayWebhookDeliveryTool = {
|
|
|
176
176
|
idempotentHint: false,
|
|
177
177
|
openWorldHint: false,
|
|
178
178
|
},
|
|
179
|
-
}, async ({ webhookId: id, deliveryId }) => {
|
|
179
|
+
}, async ({ webhookId: id, deliveryId }, { sendora }) => {
|
|
180
180
|
try {
|
|
181
|
-
const delivery = await
|
|
181
|
+
const delivery = await sendora.webhooks.replay(id, deliveryId);
|
|
182
182
|
return {
|
|
183
183
|
content: [{ type: 'text', text: JSON.stringify(delivery) }],
|
|
184
184
|
structuredContent: delivery,
|
|
185
185
|
};
|
|
186
186
|
}
|
|
187
187
|
catch (error) {
|
|
188
|
-
return toolError(error);
|
|
188
|
+
return toolError(error, connection);
|
|
189
189
|
}
|
|
190
190
|
}),
|
|
191
191
|
};
|
|
@@ -196,7 +196,7 @@ export const createWebhookTool = {
|
|
|
196
196
|
class: 'secret',
|
|
197
197
|
reach: 'server',
|
|
198
198
|
operations: ['POST /v1/webhooks'],
|
|
199
|
-
register: (server, { connection }) => server
|
|
199
|
+
register: (server, { connection, targets }) => registerServerTool(server, targets, 'create_webhook', {
|
|
200
200
|
title: 'Add a webhook',
|
|
201
201
|
description: "Adds a webhook to this test server and answers its signing secret, shown this once: put it in the project's environment, such as SENDORA_WEBHOOK_SECRET in .env, never in a file under version control, and verify every delivery with the SDK: webhooks.receive or verifyWebhook in TypeScript, verify_webhook in Python. The API takes only a public https URL, so a local endpoint needs a tunnel. Events arrive as the simulator plays out each message.",
|
|
202
202
|
inputSchema: createWebhookInput,
|
|
@@ -207,16 +207,16 @@ export const createWebhookTool = {
|
|
|
207
207
|
idempotentHint: false,
|
|
208
208
|
openWorldHint: false,
|
|
209
209
|
},
|
|
210
|
-
}, async (request) => {
|
|
210
|
+
}, async (request, { sendora }) => {
|
|
211
211
|
try {
|
|
212
|
-
const created = await
|
|
212
|
+
const created = await sendora.webhooks.create(request);
|
|
213
213
|
return {
|
|
214
214
|
content: [{ type: 'text', text: JSON.stringify(created) }],
|
|
215
215
|
structuredContent: created,
|
|
216
216
|
};
|
|
217
217
|
}
|
|
218
218
|
catch (error) {
|
|
219
|
-
return toolError(error);
|
|
219
|
+
return toolError(error, connection);
|
|
220
220
|
}
|
|
221
221
|
}),
|
|
222
222
|
};
|
|
@@ -227,7 +227,7 @@ export const deleteWebhookTool = {
|
|
|
227
227
|
class: 'irreversible',
|
|
228
228
|
reach: 'server',
|
|
229
229
|
operations: ['DELETE /v1/webhooks/{id}'],
|
|
230
|
-
register: (server, { connection }) => server
|
|
230
|
+
register: (server, { connection, targets }) => registerServerTool(server, targets, 'delete_webhook', {
|
|
231
231
|
title: 'Delete a webhook',
|
|
232
232
|
description: 'Deletes a webhook of this test server with its secrets and deliveries; nothing more is posted to it, and it cannot be restored.',
|
|
233
233
|
inputSchema: z.object({ webhookId }),
|
|
@@ -240,12 +240,12 @@ export const deleteWebhookTool = {
|
|
|
240
240
|
idempotentHint: true,
|
|
241
241
|
openWorldHint: false,
|
|
242
242
|
},
|
|
243
|
-
}, async ({ webhookId: id }) => {
|
|
243
|
+
}, async ({ webhookId: id }, { sendora }) => {
|
|
244
244
|
try {
|
|
245
|
-
await
|
|
245
|
+
await sendora.webhooks.delete(id);
|
|
246
246
|
}
|
|
247
247
|
catch (error) {
|
|
248
|
-
return toolError(error);
|
|
248
|
+
return toolError(error, connection);
|
|
249
249
|
}
|
|
250
250
|
const output = { webhookId: id };
|
|
251
251
|
return {
|
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.4.0';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sendora/mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.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.1.0"
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|
|
31
31
|
"@anthropic-ai/mcpb": "2.1.2",
|
|
@@ -38,10 +38,10 @@
|
|
|
38
38
|
"typescript": "~7.0.2",
|
|
39
39
|
"vitest": "^4.1.11",
|
|
40
40
|
"@sendora/api": "0.0.0",
|
|
41
|
+
"@sendora/db": "0.0.0",
|
|
41
42
|
"@sendora/domain": "0.0.0",
|
|
42
43
|
"@sendora/observability": "0.0.0",
|
|
43
|
-
"@sendora/site": "0.0.0"
|
|
44
|
-
"@sendora/db": "0.0.0"
|
|
44
|
+
"@sendora/site": "0.0.0"
|
|
45
45
|
},
|
|
46
46
|
"scripts": {
|
|
47
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')\"",
|