@rndmjoker/proton-mcp 0.4.4
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/LICENSE +20 -0
- package/README.md +405 -0
- package/dist/activity.d.ts +49 -0
- package/dist/activity.js +72 -0
- package/dist/activity.js.map +1 -0
- package/dist/bridge/certificate.d.ts +75 -0
- package/dist/bridge/certificate.js +176 -0
- package/dist/bridge/certificate.js.map +1 -0
- package/dist/bridge/connection.d.ts +99 -0
- package/dist/bridge/connection.js +244 -0
- package/dist/bridge/connection.js.map +1 -0
- package/dist/bridge/errors.d.ts +35 -0
- package/dist/bridge/errors.js +56 -0
- package/dist/bridge/errors.js.map +1 -0
- package/dist/config.d.ts +92 -0
- package/dist/config.js +142 -0
- package/dist/config.js.map +1 -0
- package/dist/credentials/availability.d.ts +62 -0
- package/dist/credentials/availability.js +107 -0
- package/dist/credentials/availability.js.map +1 -0
- package/dist/credentials/encrypted-file.d.ts +50 -0
- package/dist/credentials/encrypted-file.js +156 -0
- package/dist/credentials/encrypted-file.js.map +1 -0
- package/dist/credentials/keyring.d.ts +48 -0
- package/dist/credentials/keyring.js +97 -0
- package/dist/credentials/keyring.js.map +1 -0
- package/dist/credentials/plain-file.d.ts +24 -0
- package/dist/credentials/plain-file.js +78 -0
- package/dist/credentials/plain-file.js.map +1 -0
- package/dist/credentials/probe-keyring.d.ts +11 -0
- package/dist/credentials/probe-keyring.js +57 -0
- package/dist/credentials/probe-keyring.js.map +1 -0
- package/dist/credentials/session.d.ts +15 -0
- package/dist/credentials/session.js +21 -0
- package/dist/credentials/session.js.map +1 -0
- package/dist/credentials/store.d.ts +80 -0
- package/dist/credentials/store.js +94 -0
- package/dist/credentials/store.js.map +1 -0
- package/dist/in-flight.d.ts +33 -0
- package/dist/in-flight.js +78 -0
- package/dist/in-flight.js.map +1 -0
- package/dist/mail/actions.d.ts +116 -0
- package/dist/mail/actions.js +265 -0
- package/dist/mail/actions.js.map +1 -0
- package/dist/mail/attachments.d.ts +39 -0
- package/dist/mail/attachments.js +86 -0
- package/dist/mail/attachments.js.map +1 -0
- package/dist/mail/compose.d.ts +264 -0
- package/dist/mail/compose.js +395 -0
- package/dist/mail/compose.js.map +1 -0
- package/dist/mail/drafts.d.ts +138 -0
- package/dist/mail/drafts.js +449 -0
- package/dist/mail/drafts.js.map +1 -0
- package/dist/mail/ids.d.ts +78 -0
- package/dist/mail/ids.js +135 -0
- package/dist/mail/ids.js.map +1 -0
- package/dist/mail/labels.d.ts +61 -0
- package/dist/mail/labels.js +139 -0
- package/dist/mail/labels.js.map +1 -0
- package/dist/mail/markup.d.ts +91 -0
- package/dist/mail/markup.js +265 -0
- package/dist/mail/markup.js.map +1 -0
- package/dist/mail/messages.d.ts +147 -0
- package/dist/mail/messages.js +219 -0
- package/dist/mail/messages.js.map +1 -0
- package/dist/mail/quote.d.ts +65 -0
- package/dist/mail/quote.js +157 -0
- package/dist/mail/quote.js.map +1 -0
- package/dist/mail/search.d.ts +66 -0
- package/dist/mail/search.js +107 -0
- package/dist/mail/search.js.map +1 -0
- package/dist/mail/send.d.ts +61 -0
- package/dist/mail/send.js +132 -0
- package/dist/mail/send.js.map +1 -0
- package/dist/mime/parse.d.ts +110 -0
- package/dist/mime/parse.js +194 -0
- package/dist/mime/parse.js.map +1 -0
- package/dist/server.d.ts +13 -0
- package/dist/server.js +387 -0
- package/dist/server.js.map +1 -0
- package/dist/session.d.ts +86 -0
- package/dist/session.js +220 -0
- package/dist/session.js.map +1 -0
- package/dist/settings.d.ts +52 -0
- package/dist/settings.js +80 -0
- package/dist/settings.js.map +1 -0
- package/dist/tools/actions.d.ts +21 -0
- package/dist/tools/actions.js +133 -0
- package/dist/tools/actions.js.map +1 -0
- package/dist/tools/capabilities.d.ts +52 -0
- package/dist/tools/capabilities.js +96 -0
- package/dist/tools/capabilities.js.map +1 -0
- package/dist/tools/confirm.d.ts +84 -0
- package/dist/tools/confirm.js +212 -0
- package/dist/tools/confirm.js.map +1 -0
- package/dist/tools/drafts.d.ts +21 -0
- package/dist/tools/drafts.js +199 -0
- package/dist/tools/drafts.js.map +1 -0
- package/dist/tools/failures.d.ts +46 -0
- package/dist/tools/failures.js +157 -0
- package/dist/tools/failures.js.map +1 -0
- package/dist/tools/format.d.ts +55 -0
- package/dist/tools/format.js +220 -0
- package/dist/tools/format.js.map +1 -0
- package/dist/tools/guidance.d.ts +38 -0
- package/dist/tools/guidance.js +226 -0
- package/dist/tools/guidance.js.map +1 -0
- package/dist/tools/interface.d.ts +28 -0
- package/dist/tools/interface.js +63 -0
- package/dist/tools/interface.js.map +1 -0
- package/dist/tools/labels.d.ts +21 -0
- package/dist/tools/labels.js +92 -0
- package/dist/tools/labels.js.map +1 -0
- package/dist/tools/list-folders.d.ts +14 -0
- package/dist/tools/list-folders.js +70 -0
- package/dist/tools/list-folders.js.map +1 -0
- package/dist/tools/messages.d.ts +9 -0
- package/dist/tools/messages.js +221 -0
- package/dist/tools/messages.js.map +1 -0
- package/dist/tools/send.d.ts +29 -0
- package/dist/tools/send.js +236 -0
- package/dist/tools/send.js.map +1 -0
- package/dist/tools/sign-in-required.d.ts +94 -0
- package/dist/tools/sign-in-required.js +168 -0
- package/dist/tools/sign-in-required.js.map +1 -0
- package/dist/web/layout.d.ts +75 -0
- package/dist/web/layout.js +590 -0
- package/dist/web/layout.js.map +1 -0
- package/dist/web/pages.d.ts +79 -0
- package/dist/web/pages.js +284 -0
- package/dist/web/pages.js.map +1 -0
- package/dist/web/sections.d.ts +105 -0
- package/dist/web/sections.js +207 -0
- package/dist/web/sections.js.map +1 -0
- package/dist/web/security.d.ts +122 -0
- package/dist/web/security.js +180 -0
- package/dist/web/security.js.map +1 -0
- package/dist/web/server.d.ts +153 -0
- package/dist/web/server.js +622 -0
- package/dist/web/server.js.map +1 -0
- package/package.json +70 -0
package/dist/session.js
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The signed-in state of the server.
|
|
3
|
+
*
|
|
4
|
+
* Credentials used to be part of the configuration, read once at startup. That
|
|
5
|
+
* no longer works: with the web interface they can arrive at any time, and they
|
|
6
|
+
* can be revoked while the server keeps running. This is the one place that
|
|
7
|
+
* knows whether we are signed in.
|
|
8
|
+
*
|
|
9
|
+
* It also decides where credentials come from on startup, in this order:
|
|
10
|
+
*
|
|
11
|
+
* 1. Environment variables, when set. An explicit setting is never overruled.
|
|
12
|
+
* 2. The plain file, which is the interim path and the one existing setups use.
|
|
13
|
+
* 3. The system keyring, if something was stored there.
|
|
14
|
+
*
|
|
15
|
+
* The encrypted file is deliberately not in that list, because it cannot be:
|
|
16
|
+
* decrypting it needs a master password, and there is nowhere to ask for one
|
|
17
|
+
* while a stdio server starts up. It is instead reported as **locked**, which
|
|
18
|
+
* the web interface turns into a page asking for that one password. This is
|
|
19
|
+
* the whole reason `locked()` and `unlock()` exist rather than a fourth entry
|
|
20
|
+
* in the loop above.
|
|
21
|
+
*/
|
|
22
|
+
import { KeyringStore } from './credentials/keyring.js';
|
|
23
|
+
import { SessionStore } from './credentials/session.js';
|
|
24
|
+
import { PlainFileStore } from './credentials/plain-file.js';
|
|
25
|
+
import { EncryptedFileStore } from './credentials/encrypted-file.js';
|
|
26
|
+
import { BridgeError } from './bridge/errors.js';
|
|
27
|
+
/** What the encrypted store is told when nobody has supplied a master password. */
|
|
28
|
+
const NO_MASTER_PASSWORD = 'The encrypted credentials file needs a master password. Open the configuration page and ' +
|
|
29
|
+
'unlock it there.';
|
|
30
|
+
export class Session {
|
|
31
|
+
#credentials;
|
|
32
|
+
#storeKind;
|
|
33
|
+
/**
|
|
34
|
+
* The master password for the encrypted file, for as long as the process runs.
|
|
35
|
+
*
|
|
36
|
+
* Held in memory only, and only when that store is in use. Writing it
|
|
37
|
+
* anywhere would defeat the point of the option: the file is protected by
|
|
38
|
+
* something that exists nowhere else.
|
|
39
|
+
*/
|
|
40
|
+
#masterPassword;
|
|
41
|
+
#stores;
|
|
42
|
+
#notify;
|
|
43
|
+
/** Set once startup has looked for stored credentials. */
|
|
44
|
+
#restored = false;
|
|
45
|
+
constructor(options = {}) {
|
|
46
|
+
this.#notify = options.notify ?? ((m) => process.stderr.write(`proton-mcp: ${m}\n`));
|
|
47
|
+
// Taken up right away rather than on first use. Reading the environment
|
|
48
|
+
// costs nothing and cannot block, and leaving it for later made the status
|
|
49
|
+
// claim "not signed in" while the very next tool call worked.
|
|
50
|
+
if (options.fromEnvironment) {
|
|
51
|
+
this.#credentials = options.fromEnvironment;
|
|
52
|
+
this.#storeKind = 'plain-file';
|
|
53
|
+
this.#restored = true;
|
|
54
|
+
}
|
|
55
|
+
this.#stores = {
|
|
56
|
+
keyring: options.stores?.keyring ?? new KeyringStore(),
|
|
57
|
+
session: options.stores?.session ?? new SessionStore(),
|
|
58
|
+
'plain-file': options.stores?.['plain-file'] ?? new PlainFileStore(),
|
|
59
|
+
'encrypted-file': options.stores?.['encrypted-file'] ??
|
|
60
|
+
new EncryptedFileStore(async () => {
|
|
61
|
+
if (this.#masterPassword === undefined)
|
|
62
|
+
throw new BridgeError(NO_MASTER_PASSWORD);
|
|
63
|
+
return this.#masterPassword;
|
|
64
|
+
}),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
get storeKind() {
|
|
68
|
+
return this.#storeKind;
|
|
69
|
+
}
|
|
70
|
+
get signedIn() {
|
|
71
|
+
return this.#credentials !== undefined;
|
|
72
|
+
}
|
|
73
|
+
/** The address we are signed in as, for display. Never the password. */
|
|
74
|
+
get address() {
|
|
75
|
+
return this.#credentials?.user;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Returns the credentials, restoring them from a store on first use.
|
|
79
|
+
*
|
|
80
|
+
* Restoring happens here rather than at startup so that a locked keyring
|
|
81
|
+
* cannot block the server from starting. It costs one attempt on the first
|
|
82
|
+
* tool call instead.
|
|
83
|
+
*/
|
|
84
|
+
async credentials() {
|
|
85
|
+
if (this.#credentials)
|
|
86
|
+
return this.#credentials;
|
|
87
|
+
if (this.#restored)
|
|
88
|
+
return undefined;
|
|
89
|
+
this.#restored = true;
|
|
90
|
+
// Order matters: the cheap and non-blocking source first.
|
|
91
|
+
for (const kind of ['plain-file', 'keyring']) {
|
|
92
|
+
try {
|
|
93
|
+
const found = await this.#stores[kind].load();
|
|
94
|
+
if (found) {
|
|
95
|
+
this.#credentials = found;
|
|
96
|
+
this.#storeKind = kind;
|
|
97
|
+
return found;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
// A store that cannot be read is not a reason to fail: another one may
|
|
102
|
+
// work, and the web interface can always be used.
|
|
103
|
+
this.#notify(`could not read credentials from the ${kind}: ${error.message}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Whether credentials are stored that only a master password can open.
|
|
110
|
+
*
|
|
111
|
+
* Distinct from "not signed in": the user does not need to fetch their Bridge
|
|
112
|
+
* password from the Bridge again, they need to type one thing. A page that
|
|
113
|
+
* asked for everything would make the encrypted option feel broken.
|
|
114
|
+
*/
|
|
115
|
+
async locked() {
|
|
116
|
+
if (this.#credentials)
|
|
117
|
+
return false;
|
|
118
|
+
const exists = this.#stores['encrypted-file'].exists;
|
|
119
|
+
if (!exists)
|
|
120
|
+
return false;
|
|
121
|
+
try {
|
|
122
|
+
return await exists.call(this.#stores['encrypted-file']);
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
// Not being able to look is not the same as it being there.
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Opens the encrypted file with the given master password.
|
|
131
|
+
*
|
|
132
|
+
* Returns the credentials so the caller can verify them against the Bridge.
|
|
133
|
+
* On failure the password is dropped again: keeping a wrong one would make
|
|
134
|
+
* every later attempt fail with a message about the file instead of about
|
|
135
|
+
* the password.
|
|
136
|
+
*/
|
|
137
|
+
async unlock(masterPassword) {
|
|
138
|
+
if (!masterPassword)
|
|
139
|
+
throw new BridgeError('The master password is required.');
|
|
140
|
+
this.#masterPassword = masterPassword;
|
|
141
|
+
try {
|
|
142
|
+
const found = await this.#stores['encrypted-file'].load();
|
|
143
|
+
if (!found) {
|
|
144
|
+
throw new BridgeError('There is no encrypted credentials file to unlock.');
|
|
145
|
+
}
|
|
146
|
+
this.#credentials = found;
|
|
147
|
+
this.#storeKind = 'encrypted-file';
|
|
148
|
+
this.#restored = true;
|
|
149
|
+
return found;
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
this.#masterPassword = undefined;
|
|
153
|
+
throw error;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Throws away the encrypted file without opening it.
|
|
158
|
+
*
|
|
159
|
+
* The way out for someone who forgot their master password. Without it they
|
|
160
|
+
* would be stuck on the unlock page for good, because nothing anywhere can
|
|
161
|
+
* decrypt that file. What is destroyed is a Bridge password that can be
|
|
162
|
+
* looked up in the Bridge again, so this is recoverable in the only sense
|
|
163
|
+
* that matters.
|
|
164
|
+
*/
|
|
165
|
+
async discardEncrypted() {
|
|
166
|
+
await this.#stores['encrypted-file'].clear();
|
|
167
|
+
this.#masterPassword = undefined;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Stores credentials and marks the session as signed in.
|
|
171
|
+
*
|
|
172
|
+
* The caller has already verified them against the Bridge: storing something
|
|
173
|
+
* that does not work would be worse than not storing it.
|
|
174
|
+
*/
|
|
175
|
+
async signIn(credentials, storeKind,
|
|
176
|
+
/** Required for the encrypted file, meaningless for every other store. */
|
|
177
|
+
masterPassword) {
|
|
178
|
+
if (storeKind === 'encrypted-file') {
|
|
179
|
+
if (!masterPassword) {
|
|
180
|
+
throw new BridgeError('Keeping the credentials in an encrypted file needs a master password to encrypt them with.');
|
|
181
|
+
}
|
|
182
|
+
this.#masterPassword = masterPassword;
|
|
183
|
+
}
|
|
184
|
+
try {
|
|
185
|
+
await this.#stores[storeKind].save(credentials);
|
|
186
|
+
}
|
|
187
|
+
catch (error) {
|
|
188
|
+
// A master password that did not lead to a stored file must not stay
|
|
189
|
+
// behind, or the next attempt would silently use it.
|
|
190
|
+
if (storeKind === 'encrypted-file')
|
|
191
|
+
this.#masterPassword = undefined;
|
|
192
|
+
throw error;
|
|
193
|
+
}
|
|
194
|
+
this.#credentials = credentials;
|
|
195
|
+
this.#storeKind = storeKind;
|
|
196
|
+
this.#restored = true;
|
|
197
|
+
}
|
|
198
|
+
/** Forgets the credentials everywhere they might be. */
|
|
199
|
+
async signOut() {
|
|
200
|
+
// Every store, not only the one in use: a user signing out means "remove it",
|
|
201
|
+
// and a leftover in another store would silently sign them back in.
|
|
202
|
+
for (const kind of Object.keys(this.#stores)) {
|
|
203
|
+
try {
|
|
204
|
+
await this.#stores[kind].clear();
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
// Nothing there is the outcome we wanted anyway.
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
this.#credentials = undefined;
|
|
211
|
+
this.#storeKind = undefined;
|
|
212
|
+
this.#masterPassword = undefined;
|
|
213
|
+
this.#restored = true;
|
|
214
|
+
}
|
|
215
|
+
/** Replaces a store. Used by tests and by the startup wiring. */
|
|
216
|
+
setStore(kind, store) {
|
|
217
|
+
this.#stores[kind] = store;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
//# sourceMappingURL=session.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session.js","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Settings the user can change while the server runs.
|
|
3
|
+
*
|
|
4
|
+
* Only the Bridge ports, and that is the whole point of the file: the Bridge
|
|
5
|
+
* lets you change them, they are the setting most likely to be wrong, and
|
|
6
|
+
* getting them wrong looks exactly like a Bridge that is not running. Editing an
|
|
7
|
+
* environment variable in a client's configuration and restarting is a poor way
|
|
8
|
+
* to find that out.
|
|
9
|
+
*
|
|
10
|
+
* **The host is deliberately not settable here.** It stays with the environment,
|
|
11
|
+
* where changing it is a conscious act. A host field in a web form is a field
|
|
12
|
+
* that sends the Bridge password to another machine, and no convenience is worth
|
|
13
|
+
* that.
|
|
14
|
+
*
|
|
15
|
+
* The environment still wins. Someone who set BRIDGE_IMAP_PORT explicitly meant
|
|
16
|
+
* it, and having a stored value silently override it would make their setting a
|
|
17
|
+
* lie.
|
|
18
|
+
*/
|
|
19
|
+
export interface StoredSettings {
|
|
20
|
+
imapPort?: number;
|
|
21
|
+
smtpPort?: number;
|
|
22
|
+
/**
|
|
23
|
+
* Whether the "not affiliated with Proton" notice was sent away.
|
|
24
|
+
*
|
|
25
|
+
* Kept here rather than in the browser because there is no JavaScript in the
|
|
26
|
+
* interface to write a cookie, and because a setting the server holds is the
|
|
27
|
+
* same setting in every browser. It only ever hides the paragraph: the
|
|
28
|
+
* `unofficial` mark beside the name stays whatever this says.
|
|
29
|
+
*/
|
|
30
|
+
noticeDismissed?: boolean;
|
|
31
|
+
}
|
|
32
|
+
/** Where the settings live. Next to the credentials, under the user's own data. */
|
|
33
|
+
export declare function settingsPath(env?: NodeJS.ProcessEnv): string;
|
|
34
|
+
/** True for something that can be an IMAP or SMTP port. */
|
|
35
|
+
export declare function isPort(value: unknown): value is number;
|
|
36
|
+
/**
|
|
37
|
+
* Reads the stored settings.
|
|
38
|
+
*
|
|
39
|
+
* A missing, unreadable or broken file yields no settings rather than an error:
|
|
40
|
+
* these are conveniences, and none of them is worth refusing to start over.
|
|
41
|
+
* Values that are not ports are dropped individually, so one bad entry does not
|
|
42
|
+
* discard a good one.
|
|
43
|
+
*/
|
|
44
|
+
export declare function loadSettings(path?: string): Promise<StoredSettings>;
|
|
45
|
+
/**
|
|
46
|
+
* Writes the settings.
|
|
47
|
+
*
|
|
48
|
+
* Mode 600 like everything else this server puts on disk. Port numbers are not
|
|
49
|
+
* secret, but a directory of mixed permissions is how the file that does matter
|
|
50
|
+
* ends up readable one day.
|
|
51
|
+
*/
|
|
52
|
+
export declare function saveSettings(settings: StoredSettings, path?: string): Promise<void>;
|
package/dist/settings.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Settings the user can change while the server runs.
|
|
3
|
+
*
|
|
4
|
+
* Only the Bridge ports, and that is the whole point of the file: the Bridge
|
|
5
|
+
* lets you change them, they are the setting most likely to be wrong, and
|
|
6
|
+
* getting them wrong looks exactly like a Bridge that is not running. Editing an
|
|
7
|
+
* environment variable in a client's configuration and restarting is a poor way
|
|
8
|
+
* to find that out.
|
|
9
|
+
*
|
|
10
|
+
* **The host is deliberately not settable here.** It stays with the environment,
|
|
11
|
+
* where changing it is a conscious act. A host field in a web form is a field
|
|
12
|
+
* that sends the Bridge password to another machine, and no convenience is worth
|
|
13
|
+
* that.
|
|
14
|
+
*
|
|
15
|
+
* The environment still wins. Someone who set BRIDGE_IMAP_PORT explicitly meant
|
|
16
|
+
* it, and having a stored value silently override it would make their setting a
|
|
17
|
+
* lie.
|
|
18
|
+
*/
|
|
19
|
+
import { readFile, writeFile, mkdir } from 'node:fs/promises';
|
|
20
|
+
import { dirname, join } from 'node:path';
|
|
21
|
+
import { homedir } from 'node:os';
|
|
22
|
+
/** Where the settings live. Next to the credentials, under the user's own data. */
|
|
23
|
+
export function settingsPath(env = process.env) {
|
|
24
|
+
if (env.PROTON_MCP_SETTINGS_FILE)
|
|
25
|
+
return env.PROTON_MCP_SETTINGS_FILE;
|
|
26
|
+
return join(homedir(), '.config', 'proton-mcp', 'settings.json');
|
|
27
|
+
}
|
|
28
|
+
/** True for something that can be an IMAP or SMTP port. */
|
|
29
|
+
export function isPort(value) {
|
|
30
|
+
return typeof value === 'number' && Number.isInteger(value) && value >= 1 && value <= 65535;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Reads the stored settings.
|
|
34
|
+
*
|
|
35
|
+
* A missing, unreadable or broken file yields no settings rather than an error:
|
|
36
|
+
* these are conveniences, and none of them is worth refusing to start over.
|
|
37
|
+
* Values that are not ports are dropped individually, so one bad entry does not
|
|
38
|
+
* discard a good one.
|
|
39
|
+
*/
|
|
40
|
+
export async function loadSettings(path = settingsPath()) {
|
|
41
|
+
let raw;
|
|
42
|
+
try {
|
|
43
|
+
raw = await readFile(path, 'utf8');
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return {};
|
|
47
|
+
}
|
|
48
|
+
let parsed;
|
|
49
|
+
try {
|
|
50
|
+
parsed = JSON.parse(raw);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return {};
|
|
54
|
+
}
|
|
55
|
+
if (typeof parsed !== 'object' || parsed === null)
|
|
56
|
+
return {};
|
|
57
|
+
const candidate = parsed;
|
|
58
|
+
const settings = {};
|
|
59
|
+
if (isPort(candidate.imapPort))
|
|
60
|
+
settings.imapPort = candidate.imapPort;
|
|
61
|
+
if (isPort(candidate.smtpPort))
|
|
62
|
+
settings.smtpPort = candidate.smtpPort;
|
|
63
|
+
// Only a literal true hides it. Anything else, including a string "true" from
|
|
64
|
+
// a hand-edited file, leaves the notice where it is.
|
|
65
|
+
if (candidate.noticeDismissed === true)
|
|
66
|
+
settings.noticeDismissed = true;
|
|
67
|
+
return settings;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Writes the settings.
|
|
71
|
+
*
|
|
72
|
+
* Mode 600 like everything else this server puts on disk. Port numbers are not
|
|
73
|
+
* secret, but a directory of mixed permissions is how the file that does matter
|
|
74
|
+
* ends up readable one day.
|
|
75
|
+
*/
|
|
76
|
+
export async function saveSettings(settings, path = settingsPath()) {
|
|
77
|
+
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
78
|
+
await writeFile(path, `${JSON.stringify(settings, null, 2)}\n`, { mode: 0o600 });
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=settings.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"settings.js","sourceRoot":"","sources":["../src/settings.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAA;AAC7D,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AACzC,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAA;AAgBjC,mFAAmF;AACnF,MAAM,UAAU,YAAY,CAAC,GAAG,GAAsB,OAAO,CAAC,GAAG;IAC/D,IAAI,GAAG,CAAC,wBAAwB;QAAE,OAAO,GAAG,CAAC,wBAAwB,CAAA;IACrE,OAAO,IAAI,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,YAAY,EAAE,eAAe,CAAC,CAAA;AAClE,CAAC;AAED,2DAA2D;AAC3D,MAAM,UAAU,MAAM,CAAC,KAAc;IACnC,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,KAAK,IAAI,CAAC,IAAI,KAAK,IAAI,KAAK,CAAA;AAC7F,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,IAAI,GAAG,YAAY,EAAE;IACtD,IAAI,GAAW,CAAA;IACf,IAAI,CAAC;QACH,GAAG,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,CAAA;IACpC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAA;IACX,CAAC;IAED,IAAI,MAAe,CAAA;IACnB,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;IAC1B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAA;IACX,CAAC;IACD,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,IAAI;QAAE,OAAO,EAAE,CAAA;IAE5D,MAAM,SAAS,GAAG,MAAiC,CAAA;IACnD,MAAM,QAAQ,GAAmB,EAAE,CAAA;IACnC,IAAI,MAAM,CAAC,SAAS,CAAC,QAAQ,CAAC;QAAE,QAAQ,CAAC,QAAQ,GAAG,SAAS,CAAC,QAAQ,CAAA;IACtE,IAAI,MAAM,CAAC,SAAS,CAAC,QAAQ,CAAC;QAAE,QAAQ,CAAC,QAAQ,GAAG,SAAS,CAAC,QAAQ,CAAA;IACtE,8EAA8E;IAC9E,qDAAqD;IACrD,IAAI,SAAS,CAAC,eAAe,KAAK,IAAI;QAAE,QAAQ,CAAC,eAAe,GAAG,IAAI,CAAA;IACvE,OAAO,QAAQ,CAAA;AACjB,CAAC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,QAAwB,EACxB,IAAI,GAAG,YAAY,EAAE;IAErB,MAAM,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAA;IAC5D,MAAM,SAAS,CAAC,IAAI,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAA;AAClF,CAAC"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tools: move_messages, set_flags, trash_messages.
|
|
3
|
+
*
|
|
4
|
+
* The everyday tidying actions. They change where a message sits and how it is
|
|
5
|
+
* marked, never what it says and never who has seen it, so none of them is put
|
|
6
|
+
* behind a confirmation. The confirmation requirement belongs to sending, where
|
|
7
|
+
* a wrong decision cannot be taken back.
|
|
8
|
+
*
|
|
9
|
+
* Two properties are worth stating here rather than leaving to the reader:
|
|
10
|
+
*
|
|
11
|
+
* - **Nothing in this file deletes.** "Delete" means moving to the trash, which
|
|
12
|
+
* Proton keeps and which "All Mail" still lists. A tool that emptied the trash
|
|
13
|
+
* would turn a model's mistake into a permanent one.
|
|
14
|
+
* - **Nothing reports a result it has not established.** A move takes about
|
|
15
|
+
* fifteen seconds to settle and shows the message in both places meanwhile, so
|
|
16
|
+
* the answers say what was asked for and warn against checking immediately.
|
|
17
|
+
* Flags are the exception and get no such warning: measured, they hold at once.
|
|
18
|
+
*/
|
|
19
|
+
import type { McpServer } from '@modelcontextprotocol/server';
|
|
20
|
+
import type { Connection } from '../bridge/connection.js';
|
|
21
|
+
export declare function registerActionTools(server: McpServer, connection: Connection, readOnly: () => boolean): void;
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tools: move_messages, set_flags, trash_messages.
|
|
3
|
+
*
|
|
4
|
+
* The everyday tidying actions. They change where a message sits and how it is
|
|
5
|
+
* marked, never what it says and never who has seen it, so none of them is put
|
|
6
|
+
* behind a confirmation. The confirmation requirement belongs to sending, where
|
|
7
|
+
* a wrong decision cannot be taken back.
|
|
8
|
+
*
|
|
9
|
+
* Two properties are worth stating here rather than leaving to the reader:
|
|
10
|
+
*
|
|
11
|
+
* - **Nothing in this file deletes.** "Delete" means moving to the trash, which
|
|
12
|
+
* Proton keeps and which "All Mail" still lists. A tool that emptied the trash
|
|
13
|
+
* would turn a model's mistake into a permanent one.
|
|
14
|
+
* - **Nothing reports a result it has not established.** A move takes about
|
|
15
|
+
* fifteen seconds to settle and shows the message in both places meanwhile, so
|
|
16
|
+
* the answers say what was asked for and warn against checking immediately.
|
|
17
|
+
* Flags are the exception and get no such warning: measured, they hold at once.
|
|
18
|
+
*/
|
|
19
|
+
import * as z from 'zod';
|
|
20
|
+
import { moveMessages, setFlags, trashMessages, MAX_BATCH, TRASH } from '../mail/actions.js';
|
|
21
|
+
import { formatBatch } from './format.js';
|
|
22
|
+
import { describeFailure, withSignIn } from './failures.js';
|
|
23
|
+
import { track } from '../in-flight.js';
|
|
24
|
+
const ok = (text) => ({ content: [{ type: 'text', text }] });
|
|
25
|
+
/**
|
|
26
|
+
* Appended to every answer that moved something.
|
|
27
|
+
*
|
|
28
|
+
* Measured: a move needs roughly fifteen seconds to settle, and during that
|
|
29
|
+
* window the Bridge lists the message in the old place as well as the new one. A
|
|
30
|
+
* model that checks its own work straight away reads that intermediate state and
|
|
31
|
+
* reports it as the outcome.
|
|
32
|
+
*/
|
|
33
|
+
const SETTLE_NOTE = 'The Bridge needs about fifteen seconds to reconcile a move with Proton, and until then it lists ' +
|
|
34
|
+
'the message in both the old and the new mailbox. Do not check the result immediately; an ' +
|
|
35
|
+
'immediate listing would be misleading rather than informative.';
|
|
36
|
+
const messageIds = z
|
|
37
|
+
.array(z.string())
|
|
38
|
+
.min(1)
|
|
39
|
+
.max(MAX_BATCH)
|
|
40
|
+
.describe(`The message ids, as they came from a listing or a search. At most ${MAX_BATCH} per call.`);
|
|
41
|
+
const mailboxHint = z
|
|
42
|
+
.string()
|
|
43
|
+
.optional()
|
|
44
|
+
.describe('The mailbox the messages are in, if they share one. Only a shortcut: without it every ' +
|
|
45
|
+
'message is looked for across the mailboxes it could be in, which costs a request each.');
|
|
46
|
+
export function registerActionTools(server, connection, readOnly) {
|
|
47
|
+
server.registerTool('move_messages', {
|
|
48
|
+
title: 'Move messages to a folder',
|
|
49
|
+
description: 'Moves messages into a folder. In Proton a message lives in exactly one folder, so this ' +
|
|
50
|
+
'takes it out of the one it is in. Labels are not affected and survive the move. ' +
|
|
51
|
+
'Only folders are valid destinations: moving into a label would apply the label and leave ' +
|
|
52
|
+
'the message where it is, so a label is refused here and add_label does that job. ' +
|
|
53
|
+
'The destination has to exist; this server does not create folders.',
|
|
54
|
+
inputSchema: z.object({
|
|
55
|
+
messageIds,
|
|
56
|
+
target: z
|
|
57
|
+
.string()
|
|
58
|
+
.describe('The destination folder, for example "Archive" or "Folders/Work". Use list_folders to ' +
|
|
59
|
+
'see the paths. Note that Proton fills the inbox through delivery: a message moved ' +
|
|
60
|
+
'to "INBOX" over IMAP was measured to end up elsewhere shortly afterwards.'),
|
|
61
|
+
mailbox: mailboxHint,
|
|
62
|
+
}),
|
|
63
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
64
|
+
}, async ({ messageIds: ids, target, mailbox }, ctx) => track(() => withSignIn(ctx, async () => {
|
|
65
|
+
try {
|
|
66
|
+
const result = await moveMessages(connection, readOnly(), ids, target, mailbox);
|
|
67
|
+
return ok(`${formatBatch(result, `Moved into "${result.target}":`)}\n\n${SETTLE_NOTE}`);
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
return describeFailure(error, `moving messages to "${target}"`);
|
|
71
|
+
}
|
|
72
|
+
}), 'move_messages'));
|
|
73
|
+
server.registerTool('set_flags', {
|
|
74
|
+
title: 'Set read state and star',
|
|
75
|
+
description: 'Marks messages as read or unread and sets or clears the star. At least one of the two has ' +
|
|
76
|
+
'to be given. This changes nothing about where a message sits and nothing about its ' +
|
|
77
|
+
'content. Proton shows the star as the "Starred" mailbox.',
|
|
78
|
+
inputSchema: z.object({
|
|
79
|
+
messageIds,
|
|
80
|
+
read: z
|
|
81
|
+
.boolean()
|
|
82
|
+
.optional()
|
|
83
|
+
.describe('True marks the messages as read, false as unread. Omit to leave it alone.'),
|
|
84
|
+
starred: z
|
|
85
|
+
.boolean()
|
|
86
|
+
.optional()
|
|
87
|
+
.describe('True stars the messages, false removes the star. Omit to leave it alone.'),
|
|
88
|
+
mailbox: mailboxHint,
|
|
89
|
+
}),
|
|
90
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
91
|
+
}, async ({ messageIds: ids, read, starred, mailbox }, ctx) => track(() => withSignIn(ctx, async () => {
|
|
92
|
+
try {
|
|
93
|
+
const changes = {};
|
|
94
|
+
if (read !== undefined)
|
|
95
|
+
changes.read = read;
|
|
96
|
+
if (starred !== undefined)
|
|
97
|
+
changes.starred = starred;
|
|
98
|
+
const result = await setFlags(connection, readOnly(), ids, changes, mailbox);
|
|
99
|
+
const what = [
|
|
100
|
+
read === undefined ? undefined : read ? 'read' : 'unread',
|
|
101
|
+
starred === undefined ? undefined : starred ? 'starred' : 'not starred',
|
|
102
|
+
]
|
|
103
|
+
.filter(Boolean)
|
|
104
|
+
.join(' and ');
|
|
105
|
+
// No settling note here: measured, flags are correct immediately
|
|
106
|
+
// and unchanged twenty seconds later.
|
|
107
|
+
return ok(formatBatch(result, `Set to ${what} for`));
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
return describeFailure(error, 'changing the read state or the star');
|
|
111
|
+
}
|
|
112
|
+
}), 'set_flags'));
|
|
113
|
+
server.registerTool('trash_messages', {
|
|
114
|
+
title: 'Move messages to the trash',
|
|
115
|
+
description: 'Moves messages to the trash. This is what deletion means here: the messages stay in ' +
|
|
116
|
+
`"${TRASH}" and in "All Mail" and can be moved back out with move_messages. ` +
|
|
117
|
+
'There is no tool in this server that deletes a message for good or empties the trash, ' +
|
|
118
|
+
'so that a wrong decision stays reversible.',
|
|
119
|
+
inputSchema: z.object({ messageIds, mailbox: mailboxHint }),
|
|
120
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
121
|
+
}, async ({ messageIds: ids, mailbox }, ctx) => track(() => withSignIn(ctx, async () => {
|
|
122
|
+
try {
|
|
123
|
+
const result = await trashMessages(connection, readOnly(), ids, mailbox);
|
|
124
|
+
return ok(`${formatBatch(result, `Moved to "${TRASH}":`)}\n\n` +
|
|
125
|
+
'Nothing was deleted for good. The messages can be moved back out of the trash. ' +
|
|
126
|
+
SETTLE_NOTE);
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
return describeFailure(error, 'moving messages to the trash');
|
|
130
|
+
}
|
|
131
|
+
}), 'trash_messages'));
|
|
132
|
+
}
|
|
133
|
+
//# sourceMappingURL=actions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"actions.js","sourceRoot":"","sources":["../../src/tools/actions.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the client said it can do.
|
|
3
|
+
*
|
|
4
|
+
* One module, because two places ask the same question and one of them decides
|
|
5
|
+
* whether a message may be sent. When they read different sources they give
|
|
6
|
+
* different answers, and the answer that matters is the one that refuses.
|
|
7
|
+
*
|
|
8
|
+
* ## Why this exists at all
|
|
9
|
+
*
|
|
10
|
+
* The first version of both checks read the capabilities out of the request's
|
|
11
|
+
* `_meta` envelope, under `io.modelcontextprotocol/clientCapabilities`. That key
|
|
12
|
+
* belongs to protocol revision 2026-07-28. The SDK in use here does not speak
|
|
13
|
+
* that revision at all: its newest is 2025-11-25, and on such a connection the
|
|
14
|
+
* envelope is not merely empty but absent.
|
|
15
|
+
*
|
|
16
|
+
* So the check answered "this client cannot show a form" for every client that
|
|
17
|
+
* ever connected, and every send was refused. Fail-closed, so nothing went out
|
|
18
|
+
* without a confirmation. It also meant nothing went out at all.
|
|
19
|
+
*
|
|
20
|
+
* Measured on 31.07.2026 against `serveStdio` with SDK 2.0.0:
|
|
21
|
+
*
|
|
22
|
+
* | Source | On a 2025-11-25 connection |
|
|
23
|
+
* | :--- | :--- |
|
|
24
|
+
* | `ctx.mcpReq.envelope` | absent |
|
|
25
|
+
* | `server.server.getClientCapabilities()` | the declaration, complete |
|
|
26
|
+
*
|
|
27
|
+
* And the round trip itself works: a returned `inputRequired` reaches the client
|
|
28
|
+
* as an `elicitation/create`, the answer comes back, and the handler runs a
|
|
29
|
+
* second time with it. Only the question of whether to try was broken.
|
|
30
|
+
*
|
|
31
|
+
* ## Both sources, not one
|
|
32
|
+
*
|
|
33
|
+
* The envelope is asked first and the handshake second, rather than replacing
|
|
34
|
+
* one with the other. The envelope is per request and is what a 2026-era
|
|
35
|
+
* connection carries; the handshake is per connection and is what this era has.
|
|
36
|
+
* A server that has to guess which one exists is a server that breaks on the
|
|
37
|
+
* next SDK release, in whichever direction it moves.
|
|
38
|
+
*/
|
|
39
|
+
export declare function setHandshakeCapabilities(source: () => unknown): void;
|
|
40
|
+
/** For tests, which must not inherit a source from another test. */
|
|
41
|
+
export declare function _clearHandshakeCapabilities(): void;
|
|
42
|
+
/**
|
|
43
|
+
* What this client declared, from whichever source has it.
|
|
44
|
+
*
|
|
45
|
+
* Typed as unknown on purpose. The client sent this, so its shape is a claim
|
|
46
|
+
* rather than a guarantee, and every reader narrows it by hand.
|
|
47
|
+
*/
|
|
48
|
+
export declare function declaredCapabilities(ctx: unknown): unknown;
|
|
49
|
+
/** Whether the client can show a form and give an answer back. */
|
|
50
|
+
export declare function canElicitForm(ctx: unknown): boolean;
|
|
51
|
+
/** Whether the client can put a URL in front of the user. */
|
|
52
|
+
export declare function canElicitUrl(ctx: unknown): boolean;
|