@remit/mcp-server 0.10.0 → 0.11.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/auth/scopes.js +1 -1
- package/dist/deps.d.ts +5 -3
- package/dist/deps.js +2 -0
- package/dist/filter-tools.js +6 -14
- package/dist/move-tools.d.ts +3 -0
- package/dist/move-tools.js +150 -0
- package/dist/server.js +2 -0
- package/dist/tool-result.d.ts +2 -1
- package/dist/tool-result.js +11 -0
- package/package.json +9 -9
package/dist/auth/scopes.js
CHANGED
|
@@ -4,7 +4,7 @@ export const OFFLINE_ACCESS_SCOPE = "offline_access";
|
|
|
4
4
|
export const MCP_SCOPES = [MCP_READ_SCOPE, MCP_RULES_SCOPE];
|
|
5
5
|
const SCOPE_DESCRIPTIONS = {
|
|
6
6
|
[MCP_READ_SCOPE]: "Read your mail",
|
|
7
|
-
[MCP_RULES_SCOPE]: "Create and change folders and filters",
|
|
7
|
+
[MCP_RULES_SCOPE]: "Create and change folders and filters, and move mail",
|
|
8
8
|
[OFFLINE_ACCESS_SCOPE]: "Stay connected until you disconnect it",
|
|
9
9
|
};
|
|
10
10
|
export const describeScopes = (scopes) => scopes.map((scope) => ({
|
package/dist/deps.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { RemitClient } from "@remit/backend/client";
|
|
2
|
-
import type { IAccountRepository, IAddressRepository, IFilterRepository, ILabelMailboxRepository, ILabelRepository, IMailboxRepository, IMessageRepository, IOrganizeJobRequestRepository, IThreadMessageRepository, OrganizeJobEvent, QueueProducer } from "@remit/data-ports";
|
|
3
|
-
import type { BodyPartMaterializer, BodySyncCue, MailboxQueueService, MaterializeLogger, OrganizeJobLogger } from "@remit/mailbox-service";
|
|
2
|
+
import type { IAccountRepository, IAddressRepository, IFilterRepository, ILabelMailboxRepository, ILabelRepository, IMailboxRepository, IMessageLabelRepository, IMessageRepository, IOrganizeJobRequestRepository, IThreadMessageRepository, OrganizeJobEvent, QueueProducer } from "@remit/data-ports";
|
|
3
|
+
import type { BodyPartMaterializer, BodySyncCue, MailboxQueueService, MaterializeLogger, MessageMoveService, OrganizeJobLogger } from "@remit/mailbox-service";
|
|
4
4
|
import type { StorageService } from "@remit/storage-service";
|
|
5
5
|
export declare const FOLDER_SETTLE_TIMEOUT_MS = 20000;
|
|
6
6
|
export declare const FOLDER_POLL_INTERVAL_MS = 500;
|
|
@@ -18,9 +18,11 @@ export interface McpDeps {
|
|
|
18
18
|
account: Pick<IAccountRepository, "get" | "listAllByAccountConfig" | "listMailSourcesByAccountConfig">;
|
|
19
19
|
mailbox: Pick<IMailboxRepository, "get" | "listAllByAccount" | "findByPath" | "resolveAccountId">;
|
|
20
20
|
address: Pick<IAddressRepository, "countCategoryOverridesSince">;
|
|
21
|
-
label: Pick<ILabelRepository, "create" | "findByNormalizedName" | "listByAccountConfig">;
|
|
21
|
+
label: Pick<ILabelRepository, "create" | "findByNormalizedName" | "get" | "listByAccountConfig">;
|
|
22
22
|
labelMailbox: Pick<ILabelMailboxRepository, "get" | "put" | "listByAccount" | "listByLabel">;
|
|
23
23
|
message: Pick<IMessageRepository, "get" | "describe" | "countClassifiedSince">;
|
|
24
|
+
messageLabel: Pick<IMessageLabelRepository, "apply">;
|
|
25
|
+
messageMove: Pick<MessageMoveService, "moveMessages">;
|
|
24
26
|
threadMessage: Pick<IThreadMessageRepository, "listByMailbox" | "listByThread" | "searchByDate">;
|
|
25
27
|
filter: Pick<IFilterRepository, "listByAccountConfig" | "refreshExpiry" | "create" | "get" | "update">;
|
|
26
28
|
organizeJobRequest: Pick<IOrganizeJobRequestRepository, "create" | "get">;
|
package/dist/deps.js
CHANGED
|
@@ -7,6 +7,8 @@ export const mcpDepsOf = (client, { logger, settle }) => ({
|
|
|
7
7
|
label: client.label,
|
|
8
8
|
labelMailbox: client.labelMailbox,
|
|
9
9
|
message: client.message,
|
|
10
|
+
messageLabel: client.messageLabel,
|
|
11
|
+
messageMove: client.messageMove,
|
|
10
12
|
threadMessage: client.threadMessage,
|
|
11
13
|
filter: client.filter,
|
|
12
14
|
organizeJobRequest: client.organizeJobRequest,
|
package/dist/filter-tools.js
CHANGED
|
@@ -2,9 +2,9 @@ import { FILTER_NO_ACTION, FilterRuleV1Schema, filterClausesFromRule, MAX_FILTER
|
|
|
2
2
|
import { isNotFoundError } from "@remit/data-ports/errors";
|
|
3
3
|
import { deriveAccountConfigId } from "@remit/data-ports/id";
|
|
4
4
|
import { FilterClauseField, FilterMatchOperator, FilterScope, FilterState, } from "@remit/domain-enums";
|
|
5
|
-
import { accept, createFilter, listAccountFilters, ownedAccount, queueOrganizeJob,
|
|
5
|
+
import { accept, createFilter, listAccountFilters, ownedAccount, queueOrganizeJob, updateFilter, } from "@remit/mailbox-service";
|
|
6
6
|
import { z } from "zod";
|
|
7
|
-
import { asJson, refused, toolError } from "./tool-result.js";
|
|
7
|
+
import { asJson, notFoundRefusal, refused, toolError } from "./tool-result.js";
|
|
8
8
|
const FILTER_WRITE = {
|
|
9
9
|
readOnlyHint: false,
|
|
10
10
|
destructiveHint: false,
|
|
@@ -44,15 +44,7 @@ const absentJob = (error) => {
|
|
|
44
44
|
throw error;
|
|
45
45
|
return null;
|
|
46
46
|
};
|
|
47
|
-
const notFound = (
|
|
48
|
-
if (!isNotFoundError(error))
|
|
49
|
-
throw error;
|
|
50
|
-
const message = error instanceof Error ? error.message : "Not found";
|
|
51
|
-
return refuse({
|
|
52
|
-
kind: "NotFound",
|
|
53
|
-
message: `${message}. Check the filterId against list_filters.`,
|
|
54
|
-
});
|
|
55
|
-
};
|
|
47
|
+
const notFound = notFoundRefusal("Check the filterId against list_filters.");
|
|
56
48
|
const accountId = z.string().min(1).describe("Account from list_mailboxes");
|
|
57
49
|
const literalClauses = z
|
|
58
50
|
.array(z.object({
|
|
@@ -173,12 +165,12 @@ export const registerFilterTools = (server, deps, userId) => {
|
|
|
173
165
|
return asJson({ filter, organizeJobId: job.organizeJobId });
|
|
174
166
|
});
|
|
175
167
|
server.registerTool("get_organize_job", {
|
|
176
|
-
description: "Read an organize job that applies a filter to existing mail: its state (Pending, Running, Complete, Failed), how many messages it matched, applied and failed, and the error when it failed.",
|
|
168
|
+
description: "Read an organize job that applies a filter to existing mail or moves a folder's messages: its state (Pending, Running, Complete, Failed), how many messages it matched, applied and failed, and the error when it failed.",
|
|
177
169
|
inputSchema: {
|
|
178
170
|
organizeJobId: z
|
|
179
171
|
.string()
|
|
180
172
|
.min(1)
|
|
181
|
-
.describe("organizeJobId from create_filter or
|
|
173
|
+
.describe("organizeJobId from create_filter, update_filter, apply_filter or move_folder_messages"),
|
|
182
174
|
},
|
|
183
175
|
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
184
176
|
}, async ({ organizeJobId }) => {
|
|
@@ -186,7 +178,7 @@ export const registerFilterTools = (server, deps, userId) => {
|
|
|
186
178
|
.get(organizeJobId)
|
|
187
179
|
.catch(absentJob);
|
|
188
180
|
if (!job || job.accountConfigId !== accountConfigId) {
|
|
189
|
-
return toolError(`Organize job not found: ${organizeJobId}. Use the organizeJobId create_filter or
|
|
181
|
+
return toolError(`Organize job not found: ${organizeJobId}. Use the organizeJobId a create_filter, update_filter, apply_filter or move_folder_messages call returned.`);
|
|
190
182
|
}
|
|
191
183
|
return asJson(toOrganizeJob(job));
|
|
192
184
|
});
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { ORGANIZE_MATCH_LIMIT, organizePredicateRejection, } from "@remit/backend/organize";
|
|
2
|
+
import { FILTER_NO_ACTION, FilterRuleV1Schema, filterRuleFromClauses, filterRuleLimitRefusal, } from "@remit/data-ports";
|
|
3
|
+
import { deriveAccountConfigId } from "@remit/data-ports/id";
|
|
4
|
+
import { FilterMatchOperator } from "@remit/domain-enums";
|
|
5
|
+
import { accept, folderInAccount, invalid, moveMessagesTo, ownedAccount, queueOrganizeJob, resolveMoveDestination, } from "@remit/mailbox-service";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import { asJson, notFoundRefusal, refused } from "./tool-result.js";
|
|
8
|
+
const MAX_MOVE_MESSAGES = 500;
|
|
9
|
+
const MOVE_WRITE = {
|
|
10
|
+
readOnlyHint: false,
|
|
11
|
+
destructiveHint: false,
|
|
12
|
+
idempotentHint: true,
|
|
13
|
+
openWorldHint: false,
|
|
14
|
+
};
|
|
15
|
+
const notFound = notFoundRefusal("");
|
|
16
|
+
const EVERY_MESSAGE = filterRuleFromClauses({
|
|
17
|
+
matchOperator: FilterMatchOperator.And,
|
|
18
|
+
literalClauses: [],
|
|
19
|
+
});
|
|
20
|
+
const targetMailboxId = z
|
|
21
|
+
.string()
|
|
22
|
+
.min(1)
|
|
23
|
+
.optional()
|
|
24
|
+
.describe("Folder from list_mailboxes to move into. Send this or labelId.");
|
|
25
|
+
const targetLabelId = z
|
|
26
|
+
.string()
|
|
27
|
+
.min(1)
|
|
28
|
+
.optional()
|
|
29
|
+
.describe("Label from list_labels: the messages move into its folder in their account and get the label. Send this or mailboxId.");
|
|
30
|
+
const oneTarget = (args) => {
|
|
31
|
+
if (args.mailboxId && args.labelId) {
|
|
32
|
+
return invalid("Send mailboxId or labelId, not both.");
|
|
33
|
+
}
|
|
34
|
+
if (args.mailboxId)
|
|
35
|
+
return accept({ mailboxId: args.mailboxId });
|
|
36
|
+
if (args.labelId)
|
|
37
|
+
return accept({ labelId: args.labelId });
|
|
38
|
+
return invalid("Send a target: mailboxId for a folder, or labelId.");
|
|
39
|
+
};
|
|
40
|
+
const jobAction = (destination) => ({
|
|
41
|
+
actionMailboxId: destination.mailboxId,
|
|
42
|
+
actionLabelId: destination.kind === "Label" ? destination.labelId : FILTER_NO_ACTION,
|
|
43
|
+
});
|
|
44
|
+
export const registerMoveTools = (server, deps, userId) => {
|
|
45
|
+
const accountConfigId = deriveAccountConfigId(userId);
|
|
46
|
+
const organizeDeps = {
|
|
47
|
+
organizeJobRequest: deps.organizeJobRequest,
|
|
48
|
+
queue: deps.filters.organizeQueue,
|
|
49
|
+
logger: deps.filters.logger,
|
|
50
|
+
};
|
|
51
|
+
const ruleFor = async (args) => {
|
|
52
|
+
if (args.filterId && args.rule) {
|
|
53
|
+
return invalid("Send filterId or rule, not both.");
|
|
54
|
+
}
|
|
55
|
+
if (args.filterId) {
|
|
56
|
+
const filter = await deps.filter.get(accountConfigId, args.filterId);
|
|
57
|
+
return accept(filter.rule);
|
|
58
|
+
}
|
|
59
|
+
if (!args.rule)
|
|
60
|
+
return accept(EVERY_MESSAGE);
|
|
61
|
+
const tooBig = filterRuleLimitRefusal(args.rule);
|
|
62
|
+
return tooBig ? invalid(tooBig) : accept(args.rule);
|
|
63
|
+
};
|
|
64
|
+
server.registerTool("move_messages", {
|
|
65
|
+
description: "Move messages into a folder, or into a label's folder, the way the mail app moves them. A folder takes messages of its own account; a label files each account's messages into that account's label folder. Returns once the moves are queued; the mail server confirms them shortly after.",
|
|
66
|
+
inputSchema: {
|
|
67
|
+
messageIds: z
|
|
68
|
+
.array(z.string().min(1))
|
|
69
|
+
.min(1)
|
|
70
|
+
.max(MAX_MOVE_MESSAGES)
|
|
71
|
+
.describe("Messages from list_threads, search_threads or get_thread"),
|
|
72
|
+
mailboxId: targetMailboxId,
|
|
73
|
+
labelId: targetLabelId,
|
|
74
|
+
},
|
|
75
|
+
annotations: MOVE_WRITE,
|
|
76
|
+
}, async ({ messageIds, ...args }) => {
|
|
77
|
+
const target = oneTarget(args);
|
|
78
|
+
if (!target.ok)
|
|
79
|
+
return refused(target.refusal);
|
|
80
|
+
const ids = [...new Set(messageIds)];
|
|
81
|
+
const moved = await moveMessagesTo(deps, accountConfigId, ids, target.value).catch(notFound);
|
|
82
|
+
if (!moved.ok)
|
|
83
|
+
return refused(moved.refusal);
|
|
84
|
+
return asJson({
|
|
85
|
+
moved: ids.length,
|
|
86
|
+
destinations: moved.value.map(({ messageIds: _, ...group }) => group),
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
server.registerTool("move_folder_messages", {
|
|
90
|
+
description: `Move every message in one folder, or only those matching a filter's rule or a given rule, into another folder or a label's folder. Runs as an organize job: returns its organizeJobId, which get_organize_job reports on. One job moves at most ${ORGANIZE_MATCH_LIMIT} messages; call again while a finished job reports appliedCount above 0.`,
|
|
91
|
+
inputSchema: {
|
|
92
|
+
accountId: z.string().min(1).describe("Account from list_mailboxes"),
|
|
93
|
+
sourceMailboxId: z
|
|
94
|
+
.string()
|
|
95
|
+
.min(1)
|
|
96
|
+
.describe("Folder from list_mailboxes to move messages out of"),
|
|
97
|
+
mailboxId: targetMailboxId,
|
|
98
|
+
labelId: targetLabelId,
|
|
99
|
+
filterId: z
|
|
100
|
+
.string()
|
|
101
|
+
.min(1)
|
|
102
|
+
.optional()
|
|
103
|
+
.describe("Only move messages this filter's rule matches; the filter's own action is ignored"),
|
|
104
|
+
rule: FilterRuleV1Schema.optional().describe("Only move messages this rule matches, in the shape create_filter takes. Send this or filterId, or neither to move everything."),
|
|
105
|
+
},
|
|
106
|
+
annotations: { ...MOVE_WRITE, idempotentHint: false },
|
|
107
|
+
}, async ({ accountId, sourceMailboxId, mailboxId, labelId, ...match }) => {
|
|
108
|
+
const target = oneTarget({ mailboxId, labelId });
|
|
109
|
+
if (!target.ok)
|
|
110
|
+
return refused(target.refusal);
|
|
111
|
+
const owned = await deps.account
|
|
112
|
+
.get(accountId)
|
|
113
|
+
.then((account) => ownedAccount(account, accountConfigId, "act"))
|
|
114
|
+
.catch(notFound);
|
|
115
|
+
if (!owned.ok)
|
|
116
|
+
return refused(owned.refusal);
|
|
117
|
+
const source = await deps.mailbox
|
|
118
|
+
.get(accountId, sourceMailboxId)
|
|
119
|
+
.then((folder) => folderInAccount(folder, accountId, "act"))
|
|
120
|
+
.catch(notFound);
|
|
121
|
+
if (!source.ok)
|
|
122
|
+
return refused(source.refusal);
|
|
123
|
+
const destination = await resolveMoveDestination(deps, accountConfigId, accountId, target.value).catch(notFound);
|
|
124
|
+
if (!destination.ok)
|
|
125
|
+
return refused(destination.refusal);
|
|
126
|
+
const action = jobAction(destination.value);
|
|
127
|
+
if (action.actionMailboxId === sourceMailboxId) {
|
|
128
|
+
return refused(invalid("The target folder is the source folder.").refusal);
|
|
129
|
+
}
|
|
130
|
+
const rule = await ruleFor(match).catch(notFound);
|
|
131
|
+
if (!rule.ok)
|
|
132
|
+
return refused(rule.refusal);
|
|
133
|
+
const rejection = organizePredicateRejection({
|
|
134
|
+
rule: rule.value,
|
|
135
|
+
...action,
|
|
136
|
+
});
|
|
137
|
+
if (rejection)
|
|
138
|
+
return refused(invalid(rejection.message).refusal);
|
|
139
|
+
const job = await queueOrganizeJob(organizeDeps, {
|
|
140
|
+
accountConfigId,
|
|
141
|
+
userId,
|
|
142
|
+
predicate: {
|
|
143
|
+
rule: rule.value,
|
|
144
|
+
sourceMailboxId,
|
|
145
|
+
...action,
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
return asJson({ organizeJobId: job.organizeJobId, state: job.state });
|
|
149
|
+
});
|
|
150
|
+
};
|
package/dist/server.js
CHANGED
|
@@ -5,6 +5,7 @@ import { MCP_READ_SCOPE, MCP_RULES_SCOPE } from "./auth/scopes.js";
|
|
|
5
5
|
import { registerFilterTools } from "./filter-tools.js";
|
|
6
6
|
import { registerFolderTools } from "./folder-tools.js";
|
|
7
7
|
import { registerLabelReadTools, registerLabelWriteTools, } from "./label-tools.js";
|
|
8
|
+
import { registerMoveTools } from "./move-tools.js";
|
|
8
9
|
import { registerReadTools } from "./read-tools.js";
|
|
9
10
|
const unusedJsonSchemaValidator = {
|
|
10
11
|
getValidator: () => {
|
|
@@ -21,6 +22,7 @@ const buildServer = (principal, deps) => {
|
|
|
21
22
|
registerFolderTools(server, deps, deriveAccountConfigId(principal.userId));
|
|
22
23
|
registerLabelWriteTools(server, deps, deriveAccountConfigId(principal.userId));
|
|
23
24
|
registerFilterTools(server, deps, principal.userId);
|
|
25
|
+
registerMoveTools(server, deps, principal.userId);
|
|
24
26
|
}
|
|
25
27
|
return server;
|
|
26
28
|
};
|
package/dist/tool-result.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
2
|
-
import type
|
|
2
|
+
import { type Refusal, type Refused } from "@remit/mailbox-service";
|
|
3
3
|
export declare const asJson: (value: unknown) => CallToolResult;
|
|
4
4
|
export declare const toolError: (message: string) => CallToolResult;
|
|
5
5
|
export declare const refused: (refusal: Refusal) => CallToolResult;
|
|
6
|
+
export declare const notFoundRefusal: (fix: string) => (error: unknown) => Refused;
|
package/dist/tool-result.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { isNotFoundError } from "@remit/data-ports/errors";
|
|
2
|
+
import { refuse } from "@remit/mailbox-service";
|
|
1
3
|
export const asJson = (value) => ({
|
|
2
4
|
content: [{ type: "text", text: JSON.stringify(value) }],
|
|
3
5
|
});
|
|
@@ -14,3 +16,12 @@ const REFUSAL_FIX = {
|
|
|
14
16
|
};
|
|
15
17
|
const sentence = (message) => /[.!?]$/.test(message) ? message : `${message}.`;
|
|
16
18
|
export const refused = (refusal) => toolError(`${sentence(refusal.message)}${REFUSAL_FIX[refusal.kind]}`);
|
|
19
|
+
export const notFoundRefusal = (fix) => (error) => {
|
|
20
|
+
if (!isNotFoundError(error))
|
|
21
|
+
throw error;
|
|
22
|
+
const message = error instanceof Error ? error.message : "Not found";
|
|
23
|
+
return refuse({
|
|
24
|
+
kind: "NotFound",
|
|
25
|
+
message: fix ? `${sentence(message)} ${fix}` : message,
|
|
26
|
+
});
|
|
27
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remit/mcp-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "The remit MCP server: mail tools over streamable HTTP and the OAuth authorization server that guards them.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -25,12 +25,12 @@
|
|
|
25
25
|
"@better-auth/oauth-provider": "^1.7.12",
|
|
26
26
|
"@hono/node-server": "^2.1.3",
|
|
27
27
|
"@modelcontextprotocol/sdk": "1.32.1",
|
|
28
|
-
"@remit/auth-service": "0.3.
|
|
29
|
-
"@remit/data-ports": "2.
|
|
30
|
-
"@remit/domain-enums": "0.0.
|
|
31
|
-
"@remit/drizzle-service": "3.
|
|
32
|
-
"@remit/mailbox-service": "2.
|
|
33
|
-
"@remit/storage-service": "0.0.
|
|
28
|
+
"@remit/auth-service": "0.3.9",
|
|
29
|
+
"@remit/data-ports": "2.7.0",
|
|
30
|
+
"@remit/domain-enums": "0.0.51",
|
|
31
|
+
"@remit/drizzle-service": "3.5.0",
|
|
32
|
+
"@remit/mailbox-service": "2.19.0",
|
|
33
|
+
"@remit/storage-service": "0.0.37",
|
|
34
34
|
"@remit/ui": "0.0.234",
|
|
35
35
|
"better-auth": "^1.7.12",
|
|
36
36
|
"drizzle-orm": "1.0.0-rc.5",
|
|
@@ -38,12 +38,12 @@
|
|
|
38
38
|
"react": "^19",
|
|
39
39
|
"react-dom": "^19",
|
|
40
40
|
"zod": "^4.6.7",
|
|
41
|
-
"@remit/backend": "5.
|
|
41
|
+
"@remit/backend": "5.5.0",
|
|
42
42
|
"@types/react": "^19",
|
|
43
43
|
"@types/react-dom": "^19"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
|
-
"@remit/imap-worker": "3.2.
|
|
46
|
+
"@remit/imap-worker": "3.2.2",
|
|
47
47
|
"@tailwindcss/vite": "^4",
|
|
48
48
|
"tailwindcss": "^4",
|
|
49
49
|
"tsx": "*",
|