@sendora/mcp 1.3.1 → 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.
@@ -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 { serverConnectionOf } from './server.js';
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.registerTool('list_suppressions', {
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 serverConnectionOf(connection).sendora.suppressions.list(query);
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
- const { kind, sendora } = serverConnectionOf(connection);
74
- server.registerTool('remove_suppression', {
75
- title: 'Lift a suppression',
76
- description: kind === 'live'
77
- ? `${REMOVE_DESCRIPTION} On this live server the address is a real person's: confirm with the user first.`
78
- : REMOVE_DESCRIPTION,
79
- inputSchema: z.object({
80
- address: z.email().describe('The address to send to again.'),
81
- streamId: z.uuid().optional().describe(STREAM),
82
- }),
83
- outputSchema: removedOutput,
84
- annotations: {
85
- readOnlyHint: false,
86
- destructiveHint: false,
87
- idempotentHint: true,
88
- openWorldHint: false,
89
- },
90
- }, async ({ address, streamId }) => {
91
- try {
92
- await sendora.suppressions.delete({ address, streamId });
93
- }
94
- catch (error) {
95
- return toolError(error);
96
- }
97
- const output = { address, streamId: streamId ?? null };
98
- return {
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
  };
@@ -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 { serverConnectionOf } from './server.js';
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.registerTool('list_webhooks', {
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 serverConnectionOf(connection).sendora.webhooks.list();
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.registerTool('list_webhook_deliveries', {
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 serverConnectionOf(connection).sendora.webhooks.deliveries(id, query);
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.registerTool('replay_webhook_delivery', {
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 serverConnectionOf(connection).sendora.webhooks.replay(id, deliveryId);
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.registerTool('create_webhook', {
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 serverConnectionOf(connection).sendora.webhooks.create(request);
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.registerTool('delete_webhook', {
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 serverConnectionOf(connection).sendora.webhooks.delete(id);
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.3.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.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.0.0"
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/observability": "0.0.0",
41
+ "@sendora/db": "0.0.0",
42
42
  "@sendora/domain": "0.0.0",
43
- "@sendora/site": "0.0.0",
44
- "@sendora/db": "0.0.0"
43
+ "@sendora/observability": "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')\"",