@remit/mcp-server 0.8.0 → 0.10.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 +5 -3
- package/dist/deps.js +2 -0
- package/dist/filter-tools.js +30 -26
- package/dist/label-tools.d.ts +4 -0
- package/dist/label-tools.js +52 -0
- package/dist/server.js +3 -0
- package/package.json +10 -10
package/dist/deps.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { RemitClient } from "@remit/backend/client";
|
|
2
|
-
import type { IAccountRepository, IAddressRepository, IFilterRepository, IMailboxRepository, IMessageRepository, IOrganizeJobRequestRepository, IThreadMessageRepository, OrganizeJobEvent, QueueProducer } from "@remit/data-ports";
|
|
2
|
+
import type { IAccountRepository, IAddressRepository, IFilterRepository, ILabelMailboxRepository, ILabelRepository, 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;
|
|
@@ -15,12 +15,14 @@ export interface FilterToolDeps {
|
|
|
15
15
|
logger: OrganizeJobLogger;
|
|
16
16
|
}
|
|
17
17
|
export interface McpDeps {
|
|
18
|
-
account: Pick<IAccountRepository, "get" | "listAllByAccountConfig">;
|
|
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">;
|
|
22
|
+
labelMailbox: Pick<ILabelMailboxRepository, "get" | "put" | "listByAccount" | "listByLabel">;
|
|
21
23
|
message: Pick<IMessageRepository, "get" | "describe" | "countClassifiedSince">;
|
|
22
24
|
threadMessage: Pick<IThreadMessageRepository, "listByMailbox" | "listByThread" | "searchByDate">;
|
|
23
|
-
filter: Pick<IFilterRepository, "
|
|
25
|
+
filter: Pick<IFilterRepository, "listByAccountConfig" | "refreshExpiry" | "create" | "get" | "update">;
|
|
24
26
|
organizeJobRequest: Pick<IOrganizeJobRequestRepository, "create" | "get">;
|
|
25
27
|
storage: Pick<StorageService, "retrieveBodyPart">;
|
|
26
28
|
bodySync: BodyPartMaterializer;
|
package/dist/deps.js
CHANGED
|
@@ -4,6 +4,8 @@ export const mcpDepsOf = (client, { logger, settle }) => ({
|
|
|
4
4
|
account: client.account,
|
|
5
5
|
mailbox: client.mailbox,
|
|
6
6
|
address: client.address,
|
|
7
|
+
label: client.label,
|
|
8
|
+
labelMailbox: client.labelMailbox,
|
|
7
9
|
message: client.message,
|
|
8
10
|
threadMessage: client.threadMessage,
|
|
9
11
|
filter: client.filter,
|
package/dist/filter-tools.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
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";
|
|
5
|
-
import { accept, createFilter,
|
|
5
|
+
import { accept, createFilter, listAccountFilters, ownedAccount, queueOrganizeJob, refuse, updateFilter, } from "@remit/mailbox-service";
|
|
6
6
|
import { z } from "zod";
|
|
7
7
|
import { asJson, refused, toolError } from "./tool-result.js";
|
|
8
8
|
const FILTER_WRITE = {
|
|
@@ -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, hasWords or classification 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,36 +95,27 @@ export const registerFilterTools = (server, deps, userId) => {
|
|
|
85
95
|
logger: deps.filters.logger,
|
|
86
96
|
};
|
|
87
97
|
server.registerTool("list_filters", {
|
|
88
|
-
description: "List the
|
|
89
|
-
inputSchema: {
|
|
90
|
-
accountId,
|
|
91
|
-
continuationToken: z
|
|
92
|
-
.string()
|
|
93
|
-
.optional()
|
|
94
|
-
.describe("Opaque cursor from a previous response"),
|
|
95
|
-
},
|
|
98
|
+
description: "List the mail filters that act on one account: 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 filter that moves mail into a folder is listed under the account that holds the folder; a filter that only labels mail, or moves it into a label's folder, is listed under every account.",
|
|
99
|
+
inputSchema: { accountId },
|
|
96
100
|
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
97
101
|
}, async (args) => {
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
if (!page.ok)
|
|
103
|
-
return refused(page.refusal);
|
|
104
|
-
return asJson({
|
|
105
|
-
items: page.value.items.map(toFilter),
|
|
106
|
-
continuationToken: page.value.continuationToken,
|
|
107
|
-
});
|
|
102
|
+
const listed = await listAccountFilters(deps, accountConfigId, args.accountId).catch(notFound);
|
|
103
|
+
if (!listed.ok)
|
|
104
|
+
return refused(listed.refusal);
|
|
105
|
+
return asJson({ items: listed.value.map(toFilter) });
|
|
108
106
|
});
|
|
109
107
|
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.",
|
|
108
|
+
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
109
|
inputSchema: {
|
|
112
110
|
accountId,
|
|
113
111
|
name: z.string().min(1).max(256).describe("Name for the filter"),
|
|
114
112
|
scope: scope.default(FilterScope.Standing),
|
|
115
113
|
expiresAt: expiresAt.optional(),
|
|
116
114
|
matchOperator: matchOperator.default(FilterMatchOperator.And),
|
|
117
|
-
literalClauses,
|
|
115
|
+
literalClauses: literalClauses.optional(),
|
|
116
|
+
rule: rule.optional(),
|
|
117
|
+
priority: priority.default(0),
|
|
118
|
+
stop: stop.default(false),
|
|
118
119
|
actionMailboxId: actionMailboxId.default(FILTER_NO_ACTION),
|
|
119
120
|
actionLabelId: actionLabelId.default(FILTER_NO_ACTION),
|
|
120
121
|
},
|
|
@@ -129,7 +130,7 @@ export const registerFilterTools = (server, deps, userId) => {
|
|
|
129
130
|
});
|
|
130
131
|
});
|
|
131
132
|
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.",
|
|
133
|
+
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
134
|
inputSchema: {
|
|
134
135
|
accountId,
|
|
135
136
|
filterId: z.string().min(1).describe("Filter from list_filters"),
|
|
@@ -138,6 +139,9 @@ export const registerFilterTools = (server, deps, userId) => {
|
|
|
138
139
|
expiresAt: expiresAt.optional(),
|
|
139
140
|
matchOperator: matchOperator.optional(),
|
|
140
141
|
literalClauses: literalClauses.optional(),
|
|
142
|
+
rule: rule.optional(),
|
|
143
|
+
priority: priority.optional(),
|
|
144
|
+
stop: stop.optional(),
|
|
141
145
|
actionMailboxId: actionMailboxId.optional(),
|
|
142
146
|
actionLabelId: actionLabelId.optional(),
|
|
143
147
|
state: z
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import type { McpDeps } from "./deps.js";
|
|
3
|
+
export declare const registerLabelReadTools: (server: McpServer, deps: McpDeps, accountConfigId: string) => void;
|
|
4
|
+
export declare const registerLabelWriteTools: (server: McpServer, deps: McpDeps, accountConfigId: string) => void;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { LabelColor } from "@remit/domain-enums";
|
|
2
|
+
import { createLabelWithFolders, listLabelsWithFolders, } from "@remit/mailbox-service";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { asJson, refused } from "./tool-result.js";
|
|
5
|
+
const READ_ONLY = { readOnlyHint: true, openWorldHint: false };
|
|
6
|
+
const LABEL_WRITE = {
|
|
7
|
+
readOnlyHint: false,
|
|
8
|
+
destructiveHint: false,
|
|
9
|
+
idempotentHint: true,
|
|
10
|
+
openWorldHint: false,
|
|
11
|
+
};
|
|
12
|
+
const toLabel = ({ label, folders }) => ({
|
|
13
|
+
labelId: label.labelId,
|
|
14
|
+
name: label.name,
|
|
15
|
+
color: label.color,
|
|
16
|
+
folders,
|
|
17
|
+
});
|
|
18
|
+
export const registerLabelReadTools = (server, deps, accountConfigId) => {
|
|
19
|
+
server.registerTool("list_labels", {
|
|
20
|
+
description: "List the user's labels. Each label lists its folder in every mail account, with state Present, Pending (the mail server has not confirmed it yet) or Missing, and the mailboxId of a folder that is there.",
|
|
21
|
+
inputSchema: {},
|
|
22
|
+
annotations: READ_ONLY,
|
|
23
|
+
}, async () => {
|
|
24
|
+
const labels = await listLabelsWithFolders(deps, accountConfigId);
|
|
25
|
+
return asJson({ labels: labels.map(toLabel) });
|
|
26
|
+
});
|
|
27
|
+
};
|
|
28
|
+
export const registerLabelWriteTools = (server, deps, accountConfigId) => {
|
|
29
|
+
server.registerTool("create_label", {
|
|
30
|
+
description: "Create a label and its folder in every mail account, under the account's remit folder. A label with the same name is reused, and calling again creates only the folders that are missing. Returns each folder's state; a Pending folder settles once the mail server confirms it, which list_labels shows.",
|
|
31
|
+
inputSchema: {
|
|
32
|
+
name: z.string().trim().min(1).max(140).describe("Label name"),
|
|
33
|
+
color: z
|
|
34
|
+
.enum(LabelColor)
|
|
35
|
+
.optional()
|
|
36
|
+
.describe("Display color, Default when left out"),
|
|
37
|
+
},
|
|
38
|
+
annotations: LABEL_WRITE,
|
|
39
|
+
}, async ({ name, color }) => {
|
|
40
|
+
const created = await createLabelWithFolders({
|
|
41
|
+
account: deps.account,
|
|
42
|
+
mailbox: deps.mailbox,
|
|
43
|
+
label: deps.label,
|
|
44
|
+
labelMailbox: deps.labelMailbox,
|
|
45
|
+
mailboxQueue: deps.folders.mailboxQueue,
|
|
46
|
+
log: deps.logger,
|
|
47
|
+
}, accountConfigId, { name, color });
|
|
48
|
+
if (!created.ok)
|
|
49
|
+
return refused(created.refusal);
|
|
50
|
+
return asJson(toLabel(created.value));
|
|
51
|
+
});
|
|
52
|
+
};
|
package/dist/server.js
CHANGED
|
@@ -4,6 +4,7 @@ import { deriveAccountConfigId } from "@remit/data-ports/id";
|
|
|
4
4
|
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
|
+
import { registerLabelReadTools, registerLabelWriteTools, } from "./label-tools.js";
|
|
7
8
|
import { registerReadTools } from "./read-tools.js";
|
|
8
9
|
const unusedJsonSchemaValidator = {
|
|
9
10
|
getValidator: () => {
|
|
@@ -14,9 +15,11 @@ const buildServer = (principal, deps) => {
|
|
|
14
15
|
const server = new McpServer({ name: "remit-reader", version: "1.0.0" }, { jsonSchemaValidator: unusedJsonSchemaValidator });
|
|
15
16
|
if (principal.scopes.includes(MCP_READ_SCOPE)) {
|
|
16
17
|
registerReadTools(server, deps, deriveAccountConfigId(principal.userId));
|
|
18
|
+
registerLabelReadTools(server, deps, deriveAccountConfigId(principal.userId));
|
|
17
19
|
}
|
|
18
20
|
if (principal.scopes.includes(MCP_RULES_SCOPE)) {
|
|
19
21
|
registerFolderTools(server, deps, deriveAccountConfigId(principal.userId));
|
|
22
|
+
registerLabelWriteTools(server, deps, deriveAccountConfigId(principal.userId));
|
|
20
23
|
registerFilterTools(server, deps, principal.userId);
|
|
21
24
|
}
|
|
22
25
|
return server;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remit/mcp-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.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": {
|
|
@@ -21,16 +21,16 @@
|
|
|
21
21
|
"mcp-login": "node --import tsx src/cli/mcp-login.ts"
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@remit/logger": "
|
|
24
|
+
"@remit/logger": "0.2.0",
|
|
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": "
|
|
29
|
-
"@remit/data-ports": "
|
|
30
|
-
"@remit/domain-enums": "
|
|
31
|
-
"@remit/drizzle-service": "
|
|
32
|
-
"@remit/mailbox-service": "
|
|
33
|
-
"@remit/storage-service": "
|
|
28
|
+
"@remit/auth-service": "0.3.7",
|
|
29
|
+
"@remit/data-ports": "2.6.0",
|
|
30
|
+
"@remit/domain-enums": "0.0.50",
|
|
31
|
+
"@remit/drizzle-service": "3.4.0",
|
|
32
|
+
"@remit/mailbox-service": "2.18.0",
|
|
33
|
+
"@remit/storage-service": "0.0.36",
|
|
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": "
|
|
41
|
+
"@remit/backend": "5.4.0",
|
|
42
42
|
"@types/react": "^19",
|
|
43
43
|
"@types/react-dom": "^19"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
|
-
"@remit/imap-worker": "
|
|
46
|
+
"@remit/imap-worker": "3.2.0",
|
|
47
47
|
"@tailwindcss/vite": "^4",
|
|
48
48
|
"tailwindcss": "^4",
|
|
49
49
|
"tsx": "*",
|