decoy-mcp-server 1.0.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.
@@ -0,0 +1,179 @@
1
+ export const forwardingEmailTools = [
2
+ {
3
+ name: "forwarding_email_list",
4
+ description: "List all forwarding email addresses for the authenticated user",
5
+ inputSchema: {
6
+ type: "object",
7
+ properties: {
8
+ access_token: {
9
+ type: "string",
10
+ description: "Access token from auth_login",
11
+ },
12
+ },
13
+ required: ["access_token"],
14
+ },
15
+ },
16
+ {
17
+ name: "forwarding_email_add",
18
+ description: "Add a new forwarding email address. This is where decoy emails can be forwarded to.",
19
+ inputSchema: {
20
+ type: "object",
21
+ properties: {
22
+ access_token: {
23
+ type: "string",
24
+ description: "Access token from auth_login",
25
+ },
26
+ email: {
27
+ type: "string",
28
+ description: "Email address to add for forwarding (e.g., 'john@gmail.com')",
29
+ },
30
+ },
31
+ required: ["access_token", "email"],
32
+ },
33
+ },
34
+ {
35
+ name: "forwarding_email_delete",
36
+ description: "Delete a forwarding email address",
37
+ inputSchema: {
38
+ type: "object",
39
+ properties: {
40
+ access_token: {
41
+ type: "string",
42
+ description: "Access token from auth_login",
43
+ },
44
+ forwarding_email_id: {
45
+ type: "string",
46
+ description: "ID of the forwarding email to delete",
47
+ },
48
+ },
49
+ required: ["access_token", "forwarding_email_id"],
50
+ },
51
+ },
52
+ ];
53
+ export async function handleForwardingEmailTool(name, args, apiUrl) {
54
+ const accessToken = args?.access_token;
55
+ if (!accessToken) {
56
+ return {
57
+ content: [{ type: "text", text: "access_token is required" }],
58
+ isError: true,
59
+ };
60
+ }
61
+ const headers = {
62
+ "Content-Type": "application/json",
63
+ Authorization: `Bearer ${accessToken}`,
64
+ };
65
+ switch (name) {
66
+ case "forwarding_email_list": {
67
+ const response = await fetch(`${apiUrl}/forwarding-emails`, {
68
+ method: "GET",
69
+ headers,
70
+ });
71
+ const data = await response.json();
72
+ if (!response.ok) {
73
+ return {
74
+ content: [{ type: "text", text: `Failed to list forwarding emails: ${JSON.stringify(data)}` }],
75
+ isError: true,
76
+ };
77
+ }
78
+ const emails = data.forwarding_emails || [];
79
+ if (emails.length === 0) {
80
+ return {
81
+ content: [{ type: "text", text: "No forwarding emails configured. Use forwarding_email_add to add one." }],
82
+ };
83
+ }
84
+ // Decode the encrypted emails for display
85
+ const summary = emails
86
+ .map((e) => {
87
+ let emailDisplay = "Unknown";
88
+ if (e.encrypted_email) {
89
+ try {
90
+ emailDisplay = Buffer.from(e.encrypted_email, "base64").toString("utf-8");
91
+ // Mask the email for privacy
92
+ const [local, domain] = emailDisplay.split("@");
93
+ if (local && domain) {
94
+ const masked = local.length > 4
95
+ ? local.substring(0, 4) + "•••"
96
+ : local + "•••";
97
+ emailDisplay = `${masked}@${domain}`;
98
+ }
99
+ }
100
+ catch {
101
+ emailDisplay = "***";
102
+ }
103
+ }
104
+ return `- ID: ${e.id} | Email: ${emailDisplay} | Verified: ${e.is_verified ? "Yes" : "No"}`;
105
+ })
106
+ .join("\n");
107
+ return {
108
+ content: [{ type: "text", text: `Found ${emails.length} forwarding email(s):\n${summary}` }],
109
+ };
110
+ }
111
+ case "forwarding_email_add": {
112
+ const email = args?.email;
113
+ if (!email) {
114
+ return {
115
+ content: [{ type: "text", text: "email is required" }],
116
+ isError: true,
117
+ };
118
+ }
119
+ // Validate email format
120
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
121
+ if (!emailRegex.test(email)) {
122
+ return {
123
+ content: [{ type: "text", text: "Invalid email format" }],
124
+ isError: true,
125
+ };
126
+ }
127
+ // Encode the email as base64 (matching the API expectation)
128
+ const encryptedEmail = Buffer.from(email).toString("base64");
129
+ const response = await fetch(`${apiUrl}/forwarding-emails`, {
130
+ method: "POST",
131
+ headers,
132
+ body: JSON.stringify({ encrypted_email: encryptedEmail }),
133
+ });
134
+ const data = await response.json();
135
+ if (!response.ok) {
136
+ return {
137
+ content: [{ type: "text", text: `Failed to add forwarding email: ${JSON.stringify(data)}` }],
138
+ isError: true,
139
+ };
140
+ }
141
+ return {
142
+ content: [
143
+ {
144
+ type: "text",
145
+ text: `Forwarding email added!\nID: ${data.id}\nVerified: ${data.is_verified ? "Yes" : "Pending verification"}`,
146
+ },
147
+ ],
148
+ };
149
+ }
150
+ case "forwarding_email_delete": {
151
+ const forwardingEmailId = args?.forwarding_email_id;
152
+ if (!forwardingEmailId) {
153
+ return {
154
+ content: [{ type: "text", text: "forwarding_email_id is required" }],
155
+ isError: true,
156
+ };
157
+ }
158
+ const response = await fetch(`${apiUrl}/forwarding-emails?id=${forwardingEmailId}`, {
159
+ method: "DELETE",
160
+ headers,
161
+ });
162
+ if (!response.ok) {
163
+ const data = await response.json();
164
+ return {
165
+ content: [{ type: "text", text: `Failed to delete forwarding email: ${JSON.stringify(data)}` }],
166
+ isError: true,
167
+ };
168
+ }
169
+ return {
170
+ content: [{ type: "text", text: `Forwarding email ${forwardingEmailId} deleted successfully.` }],
171
+ };
172
+ }
173
+ default:
174
+ return {
175
+ content: [{ type: "text", text: `Unknown forwarding email tool: ${name}` }],
176
+ isError: true,
177
+ };
178
+ }
179
+ }
@@ -0,0 +1,9 @@
1
+ import { Tool } from "@modelcontextprotocol/sdk/types.js";
2
+ export declare const messageTools: Tool[];
3
+ export declare function handleMessageTool(name: string, args: Record<string, unknown> | undefined, apiUrl: string): Promise<{
4
+ content: Array<{
5
+ type: string;
6
+ text: string;
7
+ }>;
8
+ isError?: boolean;
9
+ }>;
@@ -0,0 +1,113 @@
1
+ export const messageTools = [
2
+ {
3
+ name: "message_list",
4
+ description: "List all messages for the authenticated user",
5
+ inputSchema: {
6
+ type: "object",
7
+ properties: {
8
+ access_token: {
9
+ type: "string",
10
+ description: "Access token from auth_login",
11
+ },
12
+ limit: {
13
+ type: "number",
14
+ description: "Maximum number of messages to return (default: 50)",
15
+ },
16
+ offset: {
17
+ type: "number",
18
+ description: "Number of messages to skip (default: 0)",
19
+ },
20
+ },
21
+ required: ["access_token"],
22
+ },
23
+ },
24
+ {
25
+ name: "message_mark_read",
26
+ description: "Mark a message as read",
27
+ inputSchema: {
28
+ type: "object",
29
+ properties: {
30
+ access_token: {
31
+ type: "string",
32
+ description: "Access token from auth_login",
33
+ },
34
+ message_id: {
35
+ type: "string",
36
+ description: "ID of the message to mark as read",
37
+ },
38
+ },
39
+ required: ["access_token", "message_id"],
40
+ },
41
+ },
42
+ ];
43
+ export async function handleMessageTool(name, args, apiUrl) {
44
+ const accessToken = args?.access_token;
45
+ if (!accessToken) {
46
+ return {
47
+ content: [{ type: "text", text: "access_token is required" }],
48
+ isError: true,
49
+ };
50
+ }
51
+ const headers = {
52
+ "Content-Type": "application/json",
53
+ Authorization: `Bearer ${accessToken}`,
54
+ };
55
+ switch (name) {
56
+ case "message_list": {
57
+ const limit = args?.limit || 50;
58
+ const offset = args?.offset || 0;
59
+ const response = await fetch(`${apiUrl}/messages?limit=${limit}&offset=${offset}`, {
60
+ method: "GET",
61
+ headers,
62
+ });
63
+ const data = await response.json();
64
+ if (!response.ok) {
65
+ return {
66
+ content: [{ type: "text", text: `Failed to list messages: ${JSON.stringify(data)}` }],
67
+ isError: true,
68
+ };
69
+ }
70
+ const messages = data.messages || [];
71
+ if (messages.length === 0) {
72
+ return {
73
+ content: [{ type: "text", text: "No messages found." }],
74
+ };
75
+ }
76
+ const summary = messages
77
+ .map((m) => `- [${m.received_at}] Decoy: ${m.decoy_id} | Channel: ${m.channel} | Read: ${m.is_read} (ID: ${m.id})`)
78
+ .join("\n");
79
+ return {
80
+ content: [{ type: "text", text: `Found ${messages.length} messages:\n${summary}` }],
81
+ };
82
+ }
83
+ case "message_mark_read": {
84
+ const messageId = args?.message_id;
85
+ if (!messageId) {
86
+ return {
87
+ content: [{ type: "text", text: "message_id is required" }],
88
+ isError: true,
89
+ };
90
+ }
91
+ const response = await fetch(`${apiUrl}/messages/${messageId}`, {
92
+ method: "PUT",
93
+ headers,
94
+ body: JSON.stringify({ is_read: true }),
95
+ });
96
+ const data = await response.json();
97
+ if (!response.ok) {
98
+ return {
99
+ content: [{ type: "text", text: `Failed to mark message as read: ${JSON.stringify(data)}` }],
100
+ isError: true,
101
+ };
102
+ }
103
+ return {
104
+ content: [{ type: "text", text: `Message ${messageId} marked as read.` }],
105
+ };
106
+ }
107
+ default:
108
+ return {
109
+ content: [{ type: "text", text: `Unknown message tool: ${name}` }],
110
+ isError: true,
111
+ };
112
+ }
113
+ }
@@ -0,0 +1,9 @@
1
+ import { Tool } from "@modelcontextprotocol/sdk/types.js";
2
+ export declare const statsTools: Tool[];
3
+ export declare function handleStatsTool(name: string, args: Record<string, unknown> | undefined, apiUrl: string): Promise<{
4
+ content: Array<{
5
+ type: string;
6
+ text: string;
7
+ }>;
8
+ isError?: boolean;
9
+ }>;
@@ -0,0 +1,61 @@
1
+ export const statsTools = [
2
+ {
3
+ name: "stats_get",
4
+ description: "Get user statistics (decoy count, messages received, spam blocked, etc.)",
5
+ inputSchema: {
6
+ type: "object",
7
+ properties: {
8
+ access_token: {
9
+ type: "string",
10
+ description: "Access token from auth_login",
11
+ },
12
+ },
13
+ required: ["access_token"],
14
+ },
15
+ },
16
+ ];
17
+ export async function handleStatsTool(name, args, apiUrl) {
18
+ const accessToken = args?.access_token;
19
+ if (!accessToken) {
20
+ return {
21
+ content: [{ type: "text", text: "access_token is required" }],
22
+ isError: true,
23
+ };
24
+ }
25
+ const headers = {
26
+ "Content-Type": "application/json",
27
+ Authorization: `Bearer ${accessToken}`,
28
+ };
29
+ switch (name) {
30
+ case "stats_get": {
31
+ const response = await fetch(`${apiUrl}/user/stats`, {
32
+ method: "GET",
33
+ headers,
34
+ });
35
+ const data = await response.json();
36
+ if (!response.ok) {
37
+ return {
38
+ content: [{ type: "text", text: `Failed to get stats: ${JSON.stringify(data)}` }],
39
+ isError: true,
40
+ };
41
+ }
42
+ const stats = `
43
+ User Statistics:
44
+ - Total Decoys: ${data.decoys_count || 0}
45
+ - Active Decoys: ${data.active_decoys || 0}
46
+ - Messages Received: ${data.messages_received || 0}
47
+ - Unread Messages: ${data.unread_count || 0}
48
+ - Spam Blocked: ${data.spam_blocked || 0}
49
+ - Last Updated: ${data.updated_at || 'N/A'}
50
+ `.trim();
51
+ return {
52
+ content: [{ type: "text", text: stats }],
53
+ };
54
+ }
55
+ default:
56
+ return {
57
+ content: [{ type: "text", text: `Unknown stats tool: ${name}` }],
58
+ isError: true,
59
+ };
60
+ }
61
+ }
@@ -0,0 +1,9 @@
1
+ import { Tool } from "@modelcontextprotocol/sdk/types.js";
2
+ export declare const subdomainTools: Tool[];
3
+ export declare function handleSubdomainTool(name: string, args: Record<string, unknown> | undefined, apiUrl: string): Promise<{
4
+ content: Array<{
5
+ type: string;
6
+ text: string;
7
+ }>;
8
+ isError?: boolean;
9
+ }>;
@@ -0,0 +1,302 @@
1
+ export const subdomainTools = [
2
+ {
3
+ name: "subdomain_status",
4
+ description: "Get the user's personal alias (subdomain) status — whether claimed, enabled, and disabled alias count",
5
+ inputSchema: {
6
+ type: "object",
7
+ properties: {
8
+ access_token: {
9
+ type: "string",
10
+ description: "Access token from auth_login",
11
+ },
12
+ },
13
+ required: ["access_token"],
14
+ },
15
+ },
16
+ {
17
+ name: "subdomain_claim",
18
+ description: "Claim a personal alias subdomain (e.g., 'yourname' gives you *@yourname.decoys.me). This is permanent and cannot be changed.",
19
+ inputSchema: {
20
+ type: "object",
21
+ properties: {
22
+ access_token: {
23
+ type: "string",
24
+ description: "Access token from auth_login",
25
+ },
26
+ subdomain: {
27
+ type: "string",
28
+ description: "The subdomain to claim (3-32 chars, lowercase alphanumeric + hyphens)",
29
+ },
30
+ },
31
+ required: ["access_token", "subdomain"],
32
+ },
33
+ },
34
+ {
35
+ name: "subdomain_toggle",
36
+ description: "Enable or disable the personal alias catch-all. When disabled, emails to *@yourname.decoys.me are not received.",
37
+ inputSchema: {
38
+ type: "object",
39
+ properties: {
40
+ access_token: {
41
+ type: "string",
42
+ description: "Access token from auth_login",
43
+ },
44
+ enabled: {
45
+ type: "boolean",
46
+ description: "true to enable, false to disable",
47
+ },
48
+ },
49
+ required: ["access_token", "enabled"],
50
+ },
51
+ },
52
+ {
53
+ name: "alias_list_disabled",
54
+ description: "List all disabled (blocked) aliases for the personal subdomain. Re-enable them with alias_enable.",
55
+ inputSchema: {
56
+ type: "object",
57
+ properties: {
58
+ access_token: {
59
+ type: "string",
60
+ description: "Access token from auth_login",
61
+ },
62
+ },
63
+ required: ["access_token"],
64
+ },
65
+ },
66
+ {
67
+ name: "alias_disable",
68
+ description: "Disable (block) a personal alias so it no longer receives emails. Reversible. e.g., disable 'netflix' to block netflix@yourname.decoys.me",
69
+ inputSchema: {
70
+ type: "object",
71
+ properties: {
72
+ access_token: {
73
+ type: "string",
74
+ description: "Access token from auth_login",
75
+ },
76
+ alias: {
77
+ type: "string",
78
+ description: "The alias local part to disable (e.g., 'netflix')",
79
+ },
80
+ },
81
+ required: ["access_token", "alias"],
82
+ },
83
+ },
84
+ {
85
+ name: "alias_enable",
86
+ description: "Restore a previously disabled alias so it receives emails again",
87
+ inputSchema: {
88
+ type: "object",
89
+ properties: {
90
+ access_token: {
91
+ type: "string",
92
+ description: "Access token from auth_login",
93
+ },
94
+ alias: {
95
+ type: "string",
96
+ description: "The alias local part to restore (e.g., 'netflix')",
97
+ },
98
+ },
99
+ required: ["access_token", "alias"],
100
+ },
101
+ },
102
+ {
103
+ name: "alias_stats",
104
+ description: "Get first-seen dates for personal aliases (when each alias first received an email)",
105
+ inputSchema: {
106
+ type: "object",
107
+ properties: {
108
+ access_token: {
109
+ type: "string",
110
+ description: "Access token from auth_login",
111
+ },
112
+ aliases: {
113
+ type: "array",
114
+ items: { type: "string" },
115
+ description: "List of alias names to check (max 100)",
116
+ },
117
+ },
118
+ required: ["access_token", "aliases"],
119
+ },
120
+ },
121
+ ];
122
+ export async function handleSubdomainTool(name, args, apiUrl) {
123
+ const accessToken = args?.access_token;
124
+ if (!accessToken) {
125
+ return {
126
+ content: [{ type: "text", text: "access_token is required" }],
127
+ isError: true,
128
+ };
129
+ }
130
+ const headers = {
131
+ "Content-Type": "application/json",
132
+ Authorization: `Bearer ${accessToken}`,
133
+ };
134
+ switch (name) {
135
+ case "subdomain_status": {
136
+ const response = await fetch(`${apiUrl}/subdomain`, {
137
+ method: "GET",
138
+ headers,
139
+ });
140
+ const data = await response.json();
141
+ if (!response.ok) {
142
+ return {
143
+ content: [{ type: "text", text: `Failed to get subdomain status: ${JSON.stringify(data)}` }],
144
+ isError: true,
145
+ };
146
+ }
147
+ return {
148
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
149
+ };
150
+ }
151
+ case "subdomain_claim": {
152
+ const subdomain = args?.subdomain;
153
+ if (!subdomain) {
154
+ return {
155
+ content: [{ type: "text", text: "subdomain is required" }],
156
+ isError: true,
157
+ };
158
+ }
159
+ const response = await fetch(`${apiUrl}/subdomain`, {
160
+ method: "PUT",
161
+ headers,
162
+ body: JSON.stringify({ subdomain }),
163
+ });
164
+ const data = await response.json();
165
+ if (!response.ok) {
166
+ return {
167
+ content: [{ type: "text", text: `Failed to claim subdomain: ${JSON.stringify(data)}` }],
168
+ isError: true,
169
+ };
170
+ }
171
+ return {
172
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
173
+ };
174
+ }
175
+ case "subdomain_toggle": {
176
+ const enabled = args?.enabled;
177
+ if (typeof enabled !== "boolean") {
178
+ return {
179
+ content: [{ type: "text", text: "enabled (boolean) is required" }],
180
+ isError: true,
181
+ };
182
+ }
183
+ const response = await fetch(`${apiUrl}/subdomain?action=toggle`, {
184
+ method: "PUT",
185
+ headers,
186
+ body: JSON.stringify({ enabled }),
187
+ });
188
+ const data = await response.json();
189
+ if (!response.ok) {
190
+ return {
191
+ content: [{ type: "text", text: `Failed to toggle subdomain: ${JSON.stringify(data)}` }],
192
+ isError: true,
193
+ };
194
+ }
195
+ return {
196
+ content: [{ type: "text", text: `Subdomain ${enabled ? "enabled" : "disabled"} successfully` }],
197
+ };
198
+ }
199
+ case "alias_list_disabled": {
200
+ const response = await fetch(`${apiUrl}/subdomain?action=disabled`, {
201
+ method: "GET",
202
+ headers,
203
+ });
204
+ const data = await response.json();
205
+ if (!response.ok) {
206
+ return {
207
+ content: [{ type: "text", text: `Failed to list disabled aliases: ${JSON.stringify(data)}` }],
208
+ isError: true,
209
+ };
210
+ }
211
+ const aliases = data.disabled_aliases || [];
212
+ if (aliases.length === 0) {
213
+ return {
214
+ content: [{ type: "text", text: "No disabled aliases found." }],
215
+ };
216
+ }
217
+ const summary = aliases
218
+ .map((a) => `- ID: ${a.id} | Disabled at: ${a.created_at}`)
219
+ .join("\n");
220
+ return {
221
+ content: [{ type: "text", text: `Found ${aliases.length} disabled aliases:\n${summary}` }],
222
+ };
223
+ }
224
+ case "alias_disable": {
225
+ const alias = args?.alias;
226
+ if (!alias) {
227
+ return {
228
+ content: [{ type: "text", text: "alias is required" }],
229
+ isError: true,
230
+ };
231
+ }
232
+ const response = await fetch(`${apiUrl}/subdomain?action=disable`, {
233
+ method: "POST",
234
+ headers,
235
+ body: JSON.stringify({ alias }),
236
+ });
237
+ const data = await response.json();
238
+ if (!response.ok) {
239
+ return {
240
+ content: [{ type: "text", text: `Failed to disable alias: ${JSON.stringify(data)}` }],
241
+ isError: true,
242
+ };
243
+ }
244
+ return {
245
+ content: [{ type: "text", text: `Alias "${alias}" disabled successfully${data.already_existed ? " (was already disabled)" : ""}` }],
246
+ };
247
+ }
248
+ case "alias_enable": {
249
+ const alias = args?.alias;
250
+ if (!alias) {
251
+ return {
252
+ content: [{ type: "text", text: "alias is required" }],
253
+ isError: true,
254
+ };
255
+ }
256
+ const response = await fetch(`${apiUrl}/subdomain?action=disable`, {
257
+ method: "DELETE",
258
+ headers,
259
+ body: JSON.stringify({ alias }),
260
+ });
261
+ const data = await response.json();
262
+ if (!response.ok) {
263
+ return {
264
+ content: [{ type: "text", text: `Failed to enable alias: ${JSON.stringify(data)}` }],
265
+ isError: true,
266
+ };
267
+ }
268
+ return {
269
+ content: [{ type: "text", text: `Alias "${alias}" restored successfully` }],
270
+ };
271
+ }
272
+ case "alias_stats": {
273
+ const aliases = args?.aliases;
274
+ if (!aliases || !Array.isArray(aliases) || aliases.length === 0) {
275
+ return {
276
+ content: [{ type: "text", text: "aliases array is required" }],
277
+ isError: true,
278
+ };
279
+ }
280
+ const response = await fetch(`${apiUrl}/subdomain?action=alias-stats`, {
281
+ method: "POST",
282
+ headers,
283
+ body: JSON.stringify({ aliases }),
284
+ });
285
+ const data = await response.json();
286
+ if (!response.ok) {
287
+ return {
288
+ content: [{ type: "text", text: `Failed to get alias stats: ${JSON.stringify(data)}` }],
289
+ isError: true,
290
+ };
291
+ }
292
+ return {
293
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
294
+ };
295
+ }
296
+ default:
297
+ return {
298
+ content: [{ type: "text", text: `Unknown subdomain tool: ${name}` }],
299
+ isError: true,
300
+ };
301
+ }
302
+ }