@remit/mcp-server 0.7.2 → 0.9.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/deps.d.ts +3 -2
- package/dist/deps.js +1 -0
- package/dist/filter-tools.js +24 -8
- package/dist/read-tools.js +53 -12
- package/package.json +1 -1
package/dist/deps.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { RemitClient } from "@remit/backend/client";
|
|
2
|
-
import type { IAccountRepository, IFilterRepository, IMailboxRepository, IMessageRepository, IOrganizeJobRequestRepository, IThreadMessageRepository, OrganizeJobEvent, QueueProducer } from "@remit/data-ports";
|
|
2
|
+
import type { IAccountRepository, IAddressRepository, IFilterRepository, IMailboxRepository, IMessageRepository, IOrganizeJobRequestRepository, IThreadMessageRepository, OrganizeJobEvent, QueueProducer } from "@remit/data-ports";
|
|
3
3
|
import type { BodyPartMaterializer, BodySyncCue, MailboxQueueService, MaterializeLogger, OrganizeJobLogger } from "@remit/mailbox-service";
|
|
4
4
|
import type { StorageService } from "@remit/storage-service";
|
|
5
5
|
export declare const FOLDER_SETTLE_TIMEOUT_MS = 20000;
|
|
@@ -17,7 +17,8 @@ export interface FilterToolDeps {
|
|
|
17
17
|
export interface McpDeps {
|
|
18
18
|
account: Pick<IAccountRepository, "get" | "listAllByAccountConfig">;
|
|
19
19
|
mailbox: Pick<IMailboxRepository, "get" | "listAllByAccount" | "findByPath" | "resolveAccountId">;
|
|
20
|
-
|
|
20
|
+
address: Pick<IAddressRepository, "countCategoryOverridesSince">;
|
|
21
|
+
message: Pick<IMessageRepository, "get" | "describe" | "countClassifiedSince">;
|
|
21
22
|
threadMessage: Pick<IThreadMessageRepository, "listByMailbox" | "listByThread" | "searchByDate">;
|
|
22
23
|
filter: Pick<IFilterRepository, "listPageByAccountConfig" | "refreshExpiry" | "create" | "get" | "update">;
|
|
23
24
|
organizeJobRequest: Pick<IOrganizeJobRequestRepository, "create" | "get">;
|
package/dist/deps.js
CHANGED
|
@@ -3,6 +3,7 @@ export const FOLDER_POLL_INTERVAL_MS = 500;
|
|
|
3
3
|
export const mcpDepsOf = (client, { logger, settle }) => ({
|
|
4
4
|
account: client.account,
|
|
5
5
|
mailbox: client.mailbox,
|
|
6
|
+
address: client.address,
|
|
6
7
|
message: client.message,
|
|
7
8
|
threadMessage: client.threadMessage,
|
|
8
9
|
filter: client.filter,
|
package/dist/filter-tools.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { FILTER_NO_ACTION, } from "@remit/data-ports";
|
|
1
|
+
import { FILTER_NO_ACTION, FilterRuleV1Schema, filterClausesFromRule, MAX_FILTER_RULE_DEPTH, MAX_FILTER_RULE_LEAVES, } from "@remit/data-ports";
|
|
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";
|
|
@@ -18,8 +18,10 @@ const toFilter = (item) => ({
|
|
|
18
18
|
expiresAt: item.expiresAt,
|
|
19
19
|
state: item.state,
|
|
20
20
|
disabledReason: item.disabledReason,
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
...filterClausesFromRule(item.rule),
|
|
22
|
+
rule: item.rule,
|
|
23
|
+
priority: item.priority,
|
|
24
|
+
stop: item.stop,
|
|
23
25
|
actionLabelId: item.actionLabelId,
|
|
24
26
|
actionMailboxId: item.actionMailboxId,
|
|
25
27
|
ruleChangedAt: item.ruleChangedAt,
|
|
@@ -59,7 +61,15 @@ const literalClauses = z
|
|
|
59
61
|
.describe("Message attribute the clause matches"),
|
|
60
62
|
value: z.string().min(1).max(256).describe("Literal text to match"),
|
|
61
63
|
}))
|
|
62
|
-
.min(1)
|
|
64
|
+
.min(1)
|
|
65
|
+
.describe("Literal clauses combined by matchOperator. Send these or rule.");
|
|
66
|
+
const rule = FilterRuleV1Schema.describe(`A Sieve-like test tree. Send this or literalClauses, never both. At most ${MAX_FILTER_RULE_LEAVES} header, address or hasWords tests and ${MAX_FILTER_RULE_DEPTH} levels, counting the top test. Example: {"version":1,"test":{"test":"allof","tests":[{"test":"address","match":"Is","part":"RegistrableDomain","key":"github.com"},{"test":"not","operand":{"test":"header","match":"Contains","header":"Subject","key":"security"}}]}}`);
|
|
67
|
+
const priority = z
|
|
68
|
+
.int()
|
|
69
|
+
.describe("Evaluation order: lower runs first; ties go to the most recently changed filter. The first matching filter with a folder decides the move, and labels add up");
|
|
70
|
+
const stop = z
|
|
71
|
+
.boolean()
|
|
72
|
+
.describe("When true, a message this filter matches is not evaluated against any later filter");
|
|
63
73
|
const scope = z
|
|
64
74
|
.enum(FilterScope)
|
|
65
75
|
.describe("Standing runs until changed; Temporary stops at expiresAt");
|
|
@@ -85,7 +95,7 @@ export const registerFilterTools = (server, deps, userId) => {
|
|
|
85
95
|
logger: deps.filters.logger,
|
|
86
96
|
};
|
|
87
97
|
server.registerTool("list_filters", {
|
|
88
|
-
description: "List the user's mail filters: what each matches, where it moves or labels mail, and whether it is on. A page at a time.",
|
|
98
|
+
description: "List the user's mail filters: what each matches (rule, plus literalClauses when the rule is a flat list), its priority and stop, where it moves or labels mail, and whether it is on. A page at a time.",
|
|
89
99
|
inputSchema: {
|
|
90
100
|
accountId,
|
|
91
101
|
continuationToken: z
|
|
@@ -107,14 +117,17 @@ export const registerFilterTools = (server, deps, userId) => {
|
|
|
107
117
|
});
|
|
108
118
|
});
|
|
109
119
|
server.registerTool("create_filter", {
|
|
110
|
-
description: "Create a filter that sorts new mail, and queue a job that applies it to existing mail. Returns the filter and the organizeJobId of that job. A target folder must be confirmed by the mail server first.",
|
|
120
|
+
description: "Create a filter that sorts new mail, and queue a job that applies it to existing mail. Match with literalClauses for a flat list, or with rule for not, nesting and exact matches. Returns the filter and the organizeJobId of that job. A target folder must be confirmed by the mail server first.",
|
|
111
121
|
inputSchema: {
|
|
112
122
|
accountId,
|
|
113
123
|
name: z.string().min(1).max(256).describe("Name for the filter"),
|
|
114
124
|
scope: scope.default(FilterScope.Standing),
|
|
115
125
|
expiresAt: expiresAt.optional(),
|
|
116
126
|
matchOperator: matchOperator.default(FilterMatchOperator.And),
|
|
117
|
-
literalClauses,
|
|
127
|
+
literalClauses: literalClauses.optional(),
|
|
128
|
+
rule: rule.optional(),
|
|
129
|
+
priority: priority.default(0),
|
|
130
|
+
stop: stop.default(false),
|
|
118
131
|
actionMailboxId: actionMailboxId.default(FILTER_NO_ACTION),
|
|
119
132
|
actionLabelId: actionLabelId.default(FILTER_NO_ACTION),
|
|
120
133
|
},
|
|
@@ -129,7 +142,7 @@ export const registerFilterTools = (server, deps, userId) => {
|
|
|
129
142
|
});
|
|
130
143
|
});
|
|
131
144
|
server.registerTool("update_filter", {
|
|
132
|
-
description: "Change a filter: its name, clauses, target folder or label, scope, expiry, or turn it on or off. Only the fields given change. Set applyToExisting to also queue a job that applies the filter to existing mail; its organizeJobId is returned.",
|
|
145
|
+
description: "Change a filter: its name, clauses or rule, priority, stop, target folder or label, scope, expiry, or turn it on or off. Only the fields given change. A filter whose rule has no clause form (literalClauses is empty in list_filters) changes only through rule. Set applyToExisting to also queue a job that applies the filter to existing mail; its organizeJobId is returned.",
|
|
133
146
|
inputSchema: {
|
|
134
147
|
accountId,
|
|
135
148
|
filterId: z.string().min(1).describe("Filter from list_filters"),
|
|
@@ -138,6 +151,9 @@ export const registerFilterTools = (server, deps, userId) => {
|
|
|
138
151
|
expiresAt: expiresAt.optional(),
|
|
139
152
|
matchOperator: matchOperator.optional(),
|
|
140
153
|
literalClauses: literalClauses.optional(),
|
|
154
|
+
rule: rule.optional(),
|
|
155
|
+
priority: priority.optional(),
|
|
156
|
+
stop: stop.optional(),
|
|
141
157
|
actionMailboxId: actionMailboxId.optional(),
|
|
142
158
|
actionLabelId: actionLabelId.optional(),
|
|
143
159
|
state: z
|
package/dist/read-tools.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { NotFoundError } from "@remit/data-ports/errors";
|
|
2
2
|
import { AddressRole, ContentDisposition, MediaType, } from "@remit/domain-enums";
|
|
3
|
-
import { materializeBodyParts, ownedMessages } from "@remit/mailbox-service";
|
|
3
|
+
import { materializeBodyParts, ownedMessages, readClassifierStats, } from "@remit/mailbox-service";
|
|
4
4
|
import { z } from "zod";
|
|
5
|
+
import { toolError } from "./tool-result.js";
|
|
5
6
|
export const MAX_BODY_BYTES = 20 * 1024;
|
|
6
7
|
const DEFAULT_PAGE_SIZE = 50;
|
|
7
8
|
const MAX_PAGE_SIZE = 50;
|
|
@@ -21,7 +22,17 @@ const continuationToken = z
|
|
|
21
22
|
.optional()
|
|
22
23
|
.describe("Opaque cursor from a previous response's continuationToken");
|
|
23
24
|
const order = z.enum(["asc", "desc"]).optional().describe("Sort order by date");
|
|
24
|
-
const
|
|
25
|
+
const classificationOf = (message, detail) => ({
|
|
26
|
+
classificationState: message.classificationState,
|
|
27
|
+
category: message.category,
|
|
28
|
+
scores: detail === "All"
|
|
29
|
+
? message.classifierScores
|
|
30
|
+
: message.classifierScores.filter((score) => score.confident),
|
|
31
|
+
});
|
|
32
|
+
const authOf = (message) => message.authResult === undefined
|
|
33
|
+
? { state: "NotParsed" }
|
|
34
|
+
: { state: "Parsed", ...message.authResult };
|
|
35
|
+
const toThreadRow = (row, message, detail) => ({
|
|
25
36
|
threadId: row.threadId,
|
|
26
37
|
messageId: row.messageId,
|
|
27
38
|
mailboxId: row.mailboxId,
|
|
@@ -33,11 +44,26 @@ const toThreadRow = (row) => ({
|
|
|
33
44
|
isRead: row.isRead,
|
|
34
45
|
hasAttachment: row.hasAttachment,
|
|
35
46
|
hasStars: row.hasStars,
|
|
47
|
+
classification: classificationOf(message, detail),
|
|
48
|
+
auth: authOf(message),
|
|
36
49
|
});
|
|
37
|
-
const toPage = (result) =>
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
50
|
+
const toPage = async (deps, result, detail) => {
|
|
51
|
+
const messages = new Map((await deps.message.get(result.items.map((row) => row.messageId))).map((message) => [message.messageId, message]));
|
|
52
|
+
const items = result.items.flatMap((row) => {
|
|
53
|
+
const message = messages.get(row.messageId);
|
|
54
|
+
return message === undefined ? [] : [toThreadRow(row, message, detail)];
|
|
55
|
+
});
|
|
56
|
+
if (items.length !== result.items.length) {
|
|
57
|
+
const missing = result.items
|
|
58
|
+
.map((row) => row.messageId)
|
|
59
|
+
.filter((messageId) => !messages.has(messageId));
|
|
60
|
+
throw new NotFoundError(`Message not found: ${missing.join(", ")}`);
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
items,
|
|
64
|
+
continuationToken: result.continuationToken,
|
|
65
|
+
};
|
|
66
|
+
};
|
|
41
67
|
const addressesByRole = (addresses, role) => addresses
|
|
42
68
|
.filter((address) => address.addressRole === role)
|
|
43
69
|
.sort((left, right) => left.addressOrder - right.addressOrder)
|
|
@@ -98,12 +124,12 @@ export const registerReadTools = (server, deps, accountConfigId) => {
|
|
|
98
124
|
order,
|
|
99
125
|
},
|
|
100
126
|
annotations: READ_ONLY,
|
|
101
|
-
}, async (args) => asJson(toPage(await deps.threadMessage.listByMailbox(accountConfigId, args.mailboxId, {
|
|
127
|
+
}, async (args) => asJson(await toPage(deps, await deps.threadMessage.listByMailbox(accountConfigId, args.mailboxId, {
|
|
102
128
|
order: args.order ?? "desc",
|
|
103
129
|
limit: args.limit ?? DEFAULT_PAGE_SIZE,
|
|
104
130
|
continuationToken: args.continuationToken,
|
|
105
131
|
excludeDeleted: true,
|
|
106
|
-
}))));
|
|
132
|
+
}), "Confident")));
|
|
107
133
|
server.registerTool("search_threads", {
|
|
108
134
|
description: "Search the user's mail by free text, with optional sender, subject and state filters; narrow to one folder with mailboxId.",
|
|
109
135
|
inputSchema: {
|
|
@@ -122,7 +148,7 @@ export const registerReadTools = (server, deps, accountConfigId) => {
|
|
|
122
148
|
order,
|
|
123
149
|
},
|
|
124
150
|
annotations: READ_ONLY,
|
|
125
|
-
}, async (args) => asJson(toPage(await deps.threadMessage.searchByDate(accountConfigId, {
|
|
151
|
+
}, async (args) => asJson(await toPage(deps, await deps.threadMessage.searchByDate(accountConfigId, {
|
|
126
152
|
query: args.query,
|
|
127
153
|
from: args.from,
|
|
128
154
|
subject: args.subject,
|
|
@@ -137,7 +163,7 @@ export const registerReadTools = (server, deps, accountConfigId) => {
|
|
|
137
163
|
? new Set([args.mailboxId])
|
|
138
164
|
: undefined,
|
|
139
165
|
excludeDeleted: true,
|
|
140
|
-
}))));
|
|
166
|
+
}), "Confident")));
|
|
141
167
|
server.registerTool("get_thread", {
|
|
142
168
|
description: "Fetch the messages of one conversation, oldest first, at envelope level.",
|
|
143
169
|
inputSchema: {
|
|
@@ -146,12 +172,27 @@ export const registerReadTools = (server, deps, accountConfigId) => {
|
|
|
146
172
|
continuationToken,
|
|
147
173
|
},
|
|
148
174
|
annotations: READ_ONLY,
|
|
149
|
-
}, async (args) => asJson(toPage(await deps.threadMessage.listByThread(args.threadId, accountConfigId, {
|
|
175
|
+
}, async (args) => asJson(await toPage(deps, await deps.threadMessage.listByThread(args.threadId, accountConfigId, {
|
|
150
176
|
order: "asc",
|
|
151
177
|
limit: args.limit ?? DEFAULT_PAGE_SIZE,
|
|
152
178
|
continuationToken: args.continuationToken,
|
|
153
179
|
excludeDeleted: true,
|
|
154
|
-
}))));
|
|
180
|
+
}), "All")));
|
|
181
|
+
server.registerTool("classifier_stats", {
|
|
182
|
+
description: "Count what the classifier decided for mail that arrived on or after a date: messages examined, messages per category, confident scores per label, and how often the user corrected it.",
|
|
183
|
+
inputSchema: {
|
|
184
|
+
since: z
|
|
185
|
+
.string()
|
|
186
|
+
.min(1)
|
|
187
|
+
.describe("Start of the window, an ISO 8601 date-time with zone offset, such as 2026-11-01T00:00:00+01:00"),
|
|
188
|
+
},
|
|
189
|
+
annotations: READ_ONLY,
|
|
190
|
+
}, async (args) => {
|
|
191
|
+
const stats = await readClassifierStats(deps, accountConfigId, args.since);
|
|
192
|
+
if (!stats.ok)
|
|
193
|
+
return toolError(`${stats.message}.`);
|
|
194
|
+
return asJson(stats.value);
|
|
195
|
+
});
|
|
155
196
|
server.registerTool("get_message", {
|
|
156
197
|
description: `Read one message: envelope, flags, attachment metadata and its plain-text body, up to ${MAX_BODY_BYTES / 1024} KB.`,
|
|
157
198
|
inputSchema: {
|