@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
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What this server tells a model about itself.
|
|
3
|
+
*
|
|
4
|
+
* Two channels, and the difference between them is the whole design here:
|
|
5
|
+
*
|
|
6
|
+
* - **`instructions`** travels with every connection whether anyone wants it or
|
|
7
|
+
* not, so it costs context on every single session. It says only what a model
|
|
8
|
+
* would otherwise get wrong before it had a reason to look anything up.
|
|
9
|
+
* - **Resources** are read on demand. They can afford to be thorough, because
|
|
10
|
+
* nobody pays for the ones that are not read.
|
|
11
|
+
*
|
|
12
|
+
* The split is not tidiness. A tool description that grows until nobody reads
|
|
13
|
+
* it protects nobody, and the same is true of instructions. So the short text
|
|
14
|
+
* names the traps and points here; the long text explains why they are traps.
|
|
15
|
+
*
|
|
16
|
+
* What goes in them is not invented. Everything below was measured against a
|
|
17
|
+
* running Bridge and a real mailbox, and the numbers are kept in because a model
|
|
18
|
+
* deciding whether to wait three seconds or twenty is better served by the
|
|
19
|
+
* measurement than by the word "briefly".
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* Sent to every client at connection time.
|
|
23
|
+
*
|
|
24
|
+
* Deliberately short. Each line is here because a model got it wrong in
|
|
25
|
+
* practice or would have: writing HTML into a plain text field, transliterating
|
|
26
|
+
* umlauts away, checking a write immediately and reporting the intermediate
|
|
27
|
+
* state as the outcome.
|
|
28
|
+
*/
|
|
29
|
+
export const INSTRUCTIONS = [
|
|
30
|
+
'This server reads and writes a Proton Mail account through a local Proton Mail Bridge.',
|
|
31
|
+
'',
|
|
32
|
+
'Four things are worth knowing before the first call, because they are the ones that go',
|
|
33
|
+
'wrong quietly:',
|
|
34
|
+
'',
|
|
35
|
+
'1. Nothing is sent without a person agreeing to it. Every sending tool asks first, through',
|
|
36
|
+
' the client, showing the final recipients. The first call never sends, and there is no',
|
|
37
|
+
' setting that turns the question off.',
|
|
38
|
+
'2. A message body is either plain text or markup, never both. Markup written into the text',
|
|
39
|
+
' field arrives as visible tags. All text is UTF-8: write words the way the language',
|
|
40
|
+
' spells them rather than transliterating characters away.',
|
|
41
|
+
'3. Writes need time to settle, and how much depends on what was written. Flags are',
|
|
42
|
+
' immediate; a move takes about fifteen seconds; a new message needs about twenty before',
|
|
43
|
+
' it appears in "All Mail". Checking straight after a write reports an intermediate state',
|
|
44
|
+
' as though it were the result.',
|
|
45
|
+
'4. Nothing here deletes for good. "Delete" means moving to the trash.',
|
|
46
|
+
'',
|
|
47
|
+
'Two guides can be read when they are needed:',
|
|
48
|
+
' proton-mcp://guide/writing composing messages, formatting, quoting, what the',
|
|
49
|
+
' confirmation shows',
|
|
50
|
+
' proton-mcp://guide/bridge how the Bridge behaves, with the measurements behind the',
|
|
51
|
+
' numbers above',
|
|
52
|
+
].join('\n');
|
|
53
|
+
const WRITING_GUIDE = `# Writing messages with this server
|
|
54
|
+
|
|
55
|
+
## Text or markup, never both
|
|
56
|
+
|
|
57
|
+
Every tool that composes a message takes either \`text\` or \`html\`. Giving both is
|
|
58
|
+
refused rather than resolved by a preference, because Proton keeps the markup and
|
|
59
|
+
drops the plain text half of a message that carries both. Whichever one this
|
|
60
|
+
server chose to prefer, the other would silently never arrive.
|
|
61
|
+
|
|
62
|
+
Markup placed in the \`text\` field is not interpreted. It arrives as visible
|
|
63
|
+
characters and the recipient reads the tags.
|
|
64
|
+
|
|
65
|
+
## What markup you may write
|
|
66
|
+
|
|
67
|
+
Headings, paragraphs, line breaks, rules, quotes, emphasis, lists, tables, links
|
|
68
|
+
and images. Styling by colour, background, font, size, weight, alignment,
|
|
69
|
+
spacing and borders. Images may point at a full address or at a file carried
|
|
70
|
+
inside the message.
|
|
71
|
+
|
|
72
|
+
Anything else is **refused, not quietly removed**, and the answer names what and
|
|
73
|
+
why. A message that was silently altered is no longer the message anyone agreed
|
|
74
|
+
to, so there is nothing to guess at: if a send is refused, the reason says which
|
|
75
|
+
element or property and what to use instead.
|
|
76
|
+
|
|
77
|
+
Two refusals are worth understanding rather than working around:
|
|
78
|
+
|
|
79
|
+
- **Properties that hide content** (\`display\`, \`visibility\`, \`opacity\`,
|
|
80
|
+
\`position\`, a size of zero) are refused. They let a message be delivered
|
|
81
|
+
looking different from how it was written.
|
|
82
|
+
- **Addresses** are limited to \`http\`, \`https\`, \`mailto\` and \`cid\`. A \`data:\`
|
|
83
|
+
address would carry a whole file inside the markup, where nothing lists it as
|
|
84
|
+
an attachment.
|
|
85
|
+
|
|
86
|
+
## What the person confirming is shown
|
|
87
|
+
|
|
88
|
+
Not only the body. Understanding this explains most of the rules above.
|
|
89
|
+
|
|
90
|
+
- The beginning of what you wrote, as it will read.
|
|
91
|
+
- **Every address in it, in full**, links and images alike, each with the text it
|
|
92
|
+
is shown as. A link's visible text and its target are the same string in plain
|
|
93
|
+
text and two different strings in markup, and that difference is the shape of
|
|
94
|
+
every phishing message ever written.
|
|
95
|
+
- **Every piece of text a recipient can read that the body preview does not
|
|
96
|
+
show.** An image's alt text is the clearest case: it never appears in the
|
|
97
|
+
converted body, and mail clients block remote images by default, so it is
|
|
98
|
+
frequently what the recipient actually reads.
|
|
99
|
+
- Every file carried with the message, with its size.
|
|
100
|
+
|
|
101
|
+
The agreement is bound to that exact message by a fingerprint covering the
|
|
102
|
+
sender, every recipient, the subject, the markup and the contents of each
|
|
103
|
+
carried file. Changing anything between the question and the answer means
|
|
104
|
+
nothing is sent. So compose the message you mean, then ask; do not ask and then
|
|
105
|
+
adjust.
|
|
106
|
+
|
|
107
|
+
## Replying and forwarding
|
|
108
|
+
|
|
109
|
+
The original is quoted below your part, with its formatting intact. You write
|
|
110
|
+
your own part and nothing else; the quote is assembled by this server from the
|
|
111
|
+
message being answered.
|
|
112
|
+
|
|
113
|
+
- Your part goes through the rules above.
|
|
114
|
+
- **The quote does not**, and could not: real mail is full of elements this
|
|
115
|
+
server would never write itself, and holding a quote to that list would make
|
|
116
|
+
replying to most messages impossible. It is kept as it was written, minus the
|
|
117
|
+
elements that would reach out of the quote and restyle your text above it.
|
|
118
|
+
- The confirmation counts the quote rather than listing it. A quoted newsletter
|
|
119
|
+
carries dozens of addresses that arrived in the recipient's mailbox already,
|
|
120
|
+
and listing those would bury the ones you are answering for.
|
|
121
|
+
|
|
122
|
+
Replying as a **draft** and replying as a **send** differ in more than whether
|
|
123
|
+
anything leaves. Proton rewrites the thread headers of anything it stores, so a
|
|
124
|
+
reply written to a draft and sent later carries Proton's idea of the
|
|
125
|
+
conversation. A reply handed straight to SMTP keeps its \`In-Reply-To\` and stays
|
|
126
|
+
in the thread. If threading matters, use \`send_reply\` rather than
|
|
127
|
+
\`reply_draft\` followed by \`send_draft\`.
|
|
128
|
+
|
|
129
|
+
## Attachments
|
|
130
|
+
|
|
131
|
+
A message cannot be given a file from this machine. That is a decision that has
|
|
132
|
+
not been made rather than a feature that is missing: which paths a model may
|
|
133
|
+
reach is its own question. Forwarding loses nothing regardless, because the
|
|
134
|
+
original travels along whole when it carries attachments.
|
|
135
|
+
`;
|
|
136
|
+
const BRIDGE_GUIDE = `# How the Bridge behaves
|
|
137
|
+
|
|
138
|
+
Everything here was measured against a running Bridge rather than read in a
|
|
139
|
+
specification, and several of these contradict what the specification would
|
|
140
|
+
suggest.
|
|
141
|
+
|
|
142
|
+
## Waiting times differ by an order of magnitude
|
|
143
|
+
|
|
144
|
+
| Operation | Before it is visible |
|
|
145
|
+
| :--- | :--- |
|
|
146
|
+
| Setting a flag, read state or star | immediately, and unchanged twenty seconds later |
|
|
147
|
+
| A message appended to a mailbox | immediately in that mailbox |
|
|
148
|
+
| The same message in "All Mail" | about twenty seconds |
|
|
149
|
+
| A move between folders, settled | about fifteen seconds |
|
|
150
|
+
| SMTP accepting a message | about six and a half seconds |
|
|
151
|
+
| A sent message visible over IMAP | about three seconds after that |
|
|
152
|
+
|
|
153
|
+
During the settling time a move shows the message in **both** mailboxes. A check
|
|
154
|
+
run straight after a write therefore reports something that is not the outcome.
|
|
155
|
+
Wait, or simply report what was asked for.
|
|
156
|
+
|
|
157
|
+
## Folders and labels are different things
|
|
158
|
+
|
|
159
|
+
A message lives in exactly one folder and carries any number of labels.
|
|
160
|
+
|
|
161
|
+
- Applying a label leaves the message where it is. Removing one leaves the
|
|
162
|
+
folder and the other labels untouched, and does not move anything to trash.
|
|
163
|
+
- **Moving a message into a label is not a move.** The label is applied and the
|
|
164
|
+
message stays in its folder. \`move_messages\` refuses a label for that reason
|
|
165
|
+
and points at \`add_label\`.
|
|
166
|
+
- Labels survive a move between folders.
|
|
167
|
+
- "All Mail" holds every message exactly once, including the ones in the trash.
|
|
168
|
+
A listing of it does not say which folder a message is really in.
|
|
169
|
+
|
|
170
|
+
## Two commands that report success without doing anything
|
|
171
|
+
|
|
172
|
+
- **A copy into a mailbox that does not exist reports success and changes
|
|
173
|
+
nothing.** No error, and the destination is not created either. Whether a
|
|
174
|
+
label exists has to be established before the call, not after it.
|
|
175
|
+
- **A move out of "All Mail" does nothing**, and reports a refusal as a falsy
|
|
176
|
+
return value rather than as an error. Moving needs the folder a message really
|
|
177
|
+
sits in.
|
|
178
|
+
|
|
179
|
+
## Other measured behaviour
|
|
180
|
+
|
|
181
|
+
- **Proton fills the inbox through delivery, not through IMAP.** A message moved
|
|
182
|
+
to "INBOX" was measured to end up elsewhere shortly afterwards.
|
|
183
|
+
- **A message with both a text and an HTML part arrives with only the HTML.**
|
|
184
|
+
- **Proton attaches its own public key to every message sent through the
|
|
185
|
+
Bridge.** It is filtered out of every attachment list here, so a client showing
|
|
186
|
+
three attachments where this server reports two is showing that key.
|
|
187
|
+
- **Proton rewrites the thread headers of anything it stores.** A draft appended
|
|
188
|
+
with \`In-Reply-To\` and \`References\` comes back without the first and with the
|
|
189
|
+
second replaced. A message handed to SMTP keeps them.
|
|
190
|
+
- **Searching is fast and local.** It runs against the Bridge's own database.
|
|
191
|
+
Ordering a listing by date is the expensive part, because there is no
|
|
192
|
+
server-side sort: it reads one date per message in the mailbox before a page
|
|
193
|
+
can be cut, and the answer says so when it took a while.
|
|
194
|
+
|
|
195
|
+
## The trap behind all of these
|
|
196
|
+
|
|
197
|
+
Every one of the surprises above looked settled after a single measurement and
|
|
198
|
+
turned out differently on the second. A move looked like a copy because the
|
|
199
|
+
message was briefly in both places. A missing label looked like a successful
|
|
200
|
+
copy because the command said so. If something here is ever re-measured, it is
|
|
201
|
+
worth doing twice with a pause in between.
|
|
202
|
+
`;
|
|
203
|
+
/**
|
|
204
|
+
* Makes the guides readable.
|
|
205
|
+
*
|
|
206
|
+
* Registered as resources rather than folded into tool descriptions, because a
|
|
207
|
+
* description is paid for on every listing whether it is needed or not, and
|
|
208
|
+
* these are long by design.
|
|
209
|
+
*/
|
|
210
|
+
export function registerGuidance(server) {
|
|
211
|
+
server.registerResource('writing-guide', 'proton-mcp://guide/writing', {
|
|
212
|
+
title: 'Writing messages with this server',
|
|
213
|
+
description: 'Composing messages: text against markup, what markup is permitted and what is refused, ' +
|
|
214
|
+
'what the person confirming a send is shown, how replies and forwards quote the original, ' +
|
|
215
|
+
'and why replying as a draft and replying as a send are not the same thing.',
|
|
216
|
+
mimeType: 'text/markdown',
|
|
217
|
+
}, async (uri) => ({ contents: [{ uri: uri.href, text: WRITING_GUIDE, mimeType: 'text/markdown' }] }));
|
|
218
|
+
server.registerResource('bridge-guide', 'proton-mcp://guide/bridge', {
|
|
219
|
+
title: 'How the Proton Mail Bridge behaves',
|
|
220
|
+
description: 'Measured behaviour of the Bridge: how long each kind of write takes to settle, how ' +
|
|
221
|
+
'folders and labels differ, and the two commands that report success without having done ' +
|
|
222
|
+
'anything.',
|
|
223
|
+
mimeType: 'text/markdown',
|
|
224
|
+
}, async (uri) => ({ contents: [{ uri: uri.href, text: BRIDGE_GUIDE, mimeType: 'text/markdown' }] }));
|
|
225
|
+
}
|
|
226
|
+
//# sourceMappingURL=guidance.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"guidance.js","sourceRoot":"","sources":["../../src/tools/guidance.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAIH;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG;IAC1B,wFAAwF;IACxF,EAAE;IACF,wFAAwF;IACxF,gBAAgB;IAChB,EAAE;IACF,4FAA4F;IAC5F,0FAA0F;IAC1F,yCAAyC;IACzC,4FAA4F;IAC5F,uFAAuF;IACvF,6DAA6D;IAC7D,oFAAoF;IACpF,2FAA2F;IAC3F,4FAA4F;IAC5F,kCAAkC;IAClC,uEAAuE;IACvE,EAAE;IACF,8CAA8C;IAC9C,kFAAkF;IAClF,mDAAmD;IACnD,yFAAyF;IACzF,8CAA8C;CAC/C,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;AAEZ,MAAM,aAAa,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAkFrB,CAAA;AAED,MAAM,YAAY,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAkEpB,CAAA;AAED;;;;;;GAMG;AACH,MAAM,UAAU,gBAAgB,CAAC,MAAiB;IAChD,MAAM,CAAC,gBAAgB,CACrB,eAAe,EACf,4BAA4B,EAC5B;QACE,KAAK,EAAE,mCAAmC;QAC1C,WAAW,EACT,yFAAyF;YACzF,2FAA2F;YAC3F,4EAA4E;QAC9E,QAAQ,EAAE,eAAe;KAC1B,EACD,KAAK,EAAE,GAAG,EAAE,EAAE,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,GAAG,EAAE,GAAG,CAAC,IAAI,EAAE,IAAI,EAAE,aAAa,EAAE,QAAQ,EAAE,eAAe,EAAE,CAAC,EAAE,CAAC,CACnG,CAAA;IAED,MAAM,CAAC,gBAAgB,CACrB,cAAc,EACd,2BAA2B,EAC3B;QACE,KAAK,EAAE,oCAAoC;QAC3C,WAAW,EACT,qFAAqF;YACrF,0FAA0F;YAC1F,WAAW;QACb,QAAQ,EAAE,eAAe;KAC1B,EACD,KAAK,EAAE,GAAG,EAAE,EAAE,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,GAAG,EAAE,GAAG,CAAC,IAAI,EAAE,IAAI,EAAE,YAAY,EAAE,QAAQ,EAAE,eAAe,EAAE,CAAC,EAAE,CAAC,CAClG,CAAA;AACH,CAAC"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool: open_configuration
|
|
3
|
+
*
|
|
4
|
+
* Hands out the address of the local web interface, including the access token.
|
|
5
|
+
*
|
|
6
|
+
* Why a tool rather than only a line on stderr: most clients hide a server's
|
|
7
|
+
* stderr, so a user who wants to reach the interface would have to go digging
|
|
8
|
+
* through logs. Asking the assistant is the obvious move, and this makes it
|
|
9
|
+
* work.
|
|
10
|
+
*
|
|
11
|
+
* The token does end up in the conversation. That is a deliberate trade: it
|
|
12
|
+
* grants access to a page on loopback that shows configuration and state, never
|
|
13
|
+
* message content, and it is worthless from any other machine. It changes with
|
|
14
|
+
* every restart. The alternative, making the user hunt for a log line, would
|
|
15
|
+
* mostly result in nobody using the interface at all.
|
|
16
|
+
*/
|
|
17
|
+
import type { McpServer } from '@modelcontextprotocol/server';
|
|
18
|
+
import type { StoreKind } from '../credentials/store.js';
|
|
19
|
+
export interface InterfaceInfo {
|
|
20
|
+
url: () => string;
|
|
21
|
+
running: () => boolean;
|
|
22
|
+
signedIn: () => boolean;
|
|
23
|
+
address: () => string | undefined;
|
|
24
|
+
storeKind: () => StoreKind | undefined;
|
|
25
|
+
/** Whether credentials are stored but still need a master password. */
|
|
26
|
+
locked?: () => Promise<boolean>;
|
|
27
|
+
}
|
|
28
|
+
export declare function registerInterfaceTool(server: McpServer, info: InterfaceInfo): void;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool: open_configuration
|
|
3
|
+
*
|
|
4
|
+
* Hands out the address of the local web interface, including the access token.
|
|
5
|
+
*
|
|
6
|
+
* Why a tool rather than only a line on stderr: most clients hide a server's
|
|
7
|
+
* stderr, so a user who wants to reach the interface would have to go digging
|
|
8
|
+
* through logs. Asking the assistant is the obvious move, and this makes it
|
|
9
|
+
* work.
|
|
10
|
+
*
|
|
11
|
+
* The token does end up in the conversation. That is a deliberate trade: it
|
|
12
|
+
* grants access to a page on loopback that shows configuration and state, never
|
|
13
|
+
* message content, and it is worthless from any other machine. It changes with
|
|
14
|
+
* every restart. The alternative, making the user hunt for a log line, would
|
|
15
|
+
* mostly result in nobody using the interface at all.
|
|
16
|
+
*/
|
|
17
|
+
import * as z from 'zod';
|
|
18
|
+
import { STORE_DESCRIPTIONS } from '../credentials/store.js';
|
|
19
|
+
import { track } from '../in-flight.js';
|
|
20
|
+
export function registerInterfaceTool(server, info) {
|
|
21
|
+
server.registerTool('open_configuration', {
|
|
22
|
+
title: 'Open the configuration interface',
|
|
23
|
+
description: 'Returns the address of the local configuration interface, which runs for as long as this ' +
|
|
24
|
+
'server does. Use it when the user asks how to sign in, where to change settings, or when ' +
|
|
25
|
+
'another tool reports that no credentials are available. The address contains an access ' +
|
|
26
|
+
'token and changes every time the server restarts.',
|
|
27
|
+
inputSchema: z.object({}),
|
|
28
|
+
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
29
|
+
}, async () => track(async () => {
|
|
30
|
+
if (!info.running()) {
|
|
31
|
+
const failure = {
|
|
32
|
+
content: [
|
|
33
|
+
{
|
|
34
|
+
type: 'text',
|
|
35
|
+
text: 'The configuration interface is not running. It failed to start, most likely ' +
|
|
36
|
+
'because its port was in use. Set PROTON_MCP_WEB_PORT to a free port and restart ' +
|
|
37
|
+
'the server. Credentials can still be provided through the BRIDGE_USER and ' +
|
|
38
|
+
'BRIDGE_PASS environment variables.',
|
|
39
|
+
},
|
|
40
|
+
],
|
|
41
|
+
isError: true,
|
|
42
|
+
};
|
|
43
|
+
return failure;
|
|
44
|
+
}
|
|
45
|
+
const lines = [`Configuration interface: ${info.url()}`, ''];
|
|
46
|
+
if (info.signedIn()) {
|
|
47
|
+
const kind = info.storeKind();
|
|
48
|
+
lines.push(`Signed in as ${info.address()}.`, kind
|
|
49
|
+
? `Credentials are kept in: ${STORE_DESCRIPTIONS[kind].title}.`
|
|
50
|
+
: 'Credentials are held for this session only.', '', 'The page shows the connection state and lets the user sign out.');
|
|
51
|
+
}
|
|
52
|
+
else if ((await info.locked?.()) === true) {
|
|
53
|
+
lines.push('Credentials are stored in an encrypted file and only need to be unlocked. Opening the', 'address above leads to a page asking for the master password, and nothing else: the', 'Bridge password is already on disk.', '', 'Tell the user to open it themselves. Do not ask them for the master password here.');
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
lines.push('Not signed in yet. Opening the address above leads to a form asking for the Proton', 'address and the Bridge password, which is the password the Bridge generates and not', 'the Proton account password.', '', 'Tell the user to open it themselves. Do not ask them for the password here: it has no', 'business in a conversation, and the page exists so that it never has to be.');
|
|
57
|
+
}
|
|
58
|
+
lines.push('', 'The address is only reachable from this machine and the token changes on every restart.');
|
|
59
|
+
const answer = { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
60
|
+
return answer;
|
|
61
|
+
}, 'open_configuration'));
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=interface.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"interface.js","sourceRoot":"","sources":["../../src/tools/interface.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,CAAC,MAAM,KAAK,CAAA;AAGxB,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AAC5D,OAAO,EAAE,KAAK,EAAE,MAAM,iBAAiB,CAAA;AAYvC,MAAM,UAAU,qBAAqB,CAAC,MAAiB,EAAE,IAAmB;IAC1E,MAAM,CAAC,YAAY,CACjB,oBAAoB,EACpB;QACE,KAAK,EAAE,kCAAkC;QACzC,WAAW,EACT,2FAA2F;YAC3F,2FAA2F;YAC3F,yFAAyF;YACzF,mDAAmD;QACrD,WAAW,EAAE,CAAC,CAAC,MAAM,CAAC,EAAE,CAAC;QACzB,WAAW,EAAE,EAAE,YAAY,EAAE,IAAI,EAAE,aAAa,EAAE,KAAK,EAAE;KAC1D,EACD,KAAK,IAAI,EAAE,CACT,KAAK,CAAC,KAAK,IAAI,EAAE;QACf,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,EAAE,CAAC;YACpB,MAAM,OAAO,GAAmB;gBAC9B,OAAO,EAAE;oBACP;wBACE,IAAI,EAAE,MAAe;wBACrB,IAAI,EACF,8EAA8E;4BAC9E,kFAAkF;4BAClF,4EAA4E;4BAC5E,oCAAoC;qBACvC;iBACF;gBACD,OAAO,EAAE,IAAI;aACd,CAAA;YACD,OAAO,OAAO,CAAA;QAChB,CAAC;QAED,MAAM,KAAK,GAAa,CAAC,4BAA4B,IAAI,CAAC,GAAG,EAAE,EAAE,EAAE,EAAE,CAAC,CAAA;QAEtE,IAAI,IAAI,CAAC,QAAQ,EAAE,EAAE,CAAC;YACpB,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,EAAE,CAAA;YAC7B,KAAK,CAAC,IAAI,CACR,gBAAgB,IAAI,CAAC,OAAO,EAAE,GAAG,EACjC,IAAI;gBACF,CAAC,CAAC,4BAA4B,kBAAkB,CAAC,IAAI,CAAC,CAAC,KAAK,GAAG;gBAC/D,CAAC,CAAC,6CAA6C,EACjD,EAAE,EACF,iEAAiE,CAClE,CAAA;QACH,CAAC;aAAM,IAAI,CAAC,MAAM,IAAI,CAAC,MAAM,EAAE,EAAE,CAAC,KAAK,IAAI,EAAE,CAAC;YAC5C,KAAK,CAAC,IAAI,CACR,uFAAuF,EACvF,qFAAqF,EACrF,qCAAqC,EACrC,EAAE,EACF,oFAAoF,CACrF,CAAA;QACH,CAAC;aAAM,CAAC;YACN,KAAK,CAAC,IAAI,CACR,oFAAoF,EACpF,qFAAqF,EACrF,8BAA8B,EAC9B,EAAE,EACF,uFAAuF,EACvF,6EAA6E,CAC9E,CAAA;QACH,CAAC;QAED,KAAK,CAAC,IAAI,CACR,EAAE,EACF,yFAAyF,CAC1F,CAAA;QAED,MAAM,MAAM,GAAmB,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,CAAA;QACtF,OAAO,MAAM,CAAA;IACf,CAAC,EAAE,oBAAoB,CAAC,CAC3B,CAAA;AACH,CAAC"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tools: add_label, remove_label
|
|
3
|
+
*
|
|
4
|
+
* The first tools that change anything. Two properties matter more than the
|
|
5
|
+
* feature itself:
|
|
6
|
+
*
|
|
7
|
+
* - They honour the read-only mode. Until now `PROTON_MCP_READ_ONLY` was a
|
|
8
|
+
* setting the web interface displayed and nothing enforced, because there was
|
|
9
|
+
* nothing to prevent. From here on the display has to be true.
|
|
10
|
+
* - They never report success they have not established. A copy into a label
|
|
11
|
+
* that does not exist is accepted by the Bridge and changes nothing, so the
|
|
12
|
+
* label is checked beforehand, and the answer says what was asked for rather
|
|
13
|
+
* than claiming a verified outcome.
|
|
14
|
+
*
|
|
15
|
+
* Labelling is not put behind a confirmation. It changes no content, sends
|
|
16
|
+
* nothing outside, and is undone by the opposite call. The confirmation
|
|
17
|
+
* requirement belongs to sending, where a wrong decision cannot be taken back.
|
|
18
|
+
*/
|
|
19
|
+
import type { McpServer } from '@modelcontextprotocol/server';
|
|
20
|
+
import type { Connection } from '../bridge/connection.js';
|
|
21
|
+
export declare function registerLabelTools(server: McpServer, connection: Connection, readOnly: () => boolean): void;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tools: add_label, remove_label
|
|
3
|
+
*
|
|
4
|
+
* The first tools that change anything. Two properties matter more than the
|
|
5
|
+
* feature itself:
|
|
6
|
+
*
|
|
7
|
+
* - They honour the read-only mode. Until now `PROTON_MCP_READ_ONLY` was a
|
|
8
|
+
* setting the web interface displayed and nothing enforced, because there was
|
|
9
|
+
* nothing to prevent. From here on the display has to be true.
|
|
10
|
+
* - They never report success they have not established. A copy into a label
|
|
11
|
+
* that does not exist is accepted by the Bridge and changes nothing, so the
|
|
12
|
+
* label is checked beforehand, and the answer says what was asked for rather
|
|
13
|
+
* than claiming a verified outcome.
|
|
14
|
+
*
|
|
15
|
+
* Labelling is not put behind a confirmation. It changes no content, sends
|
|
16
|
+
* nothing outside, and is undone by the opposite call. The confirmation
|
|
17
|
+
* requirement belongs to sending, where a wrong decision cannot be taken back.
|
|
18
|
+
*/
|
|
19
|
+
import * as z from 'zod';
|
|
20
|
+
import { addLabel, removeLabel, LABEL_PREFIX } from '../mail/labels.js';
|
|
21
|
+
import { describeFailure, withSignIn } from './failures.js';
|
|
22
|
+
import { track } from '../in-flight.js';
|
|
23
|
+
const ok = (text) => ({ content: [{ type: 'text', text }] });
|
|
24
|
+
/**
|
|
25
|
+
* Written into both answers.
|
|
26
|
+
*
|
|
27
|
+
* Measured: right after a write the Bridge reports an intermediate state. A
|
|
28
|
+
* model that checks its own result immediately will describe something that is
|
|
29
|
+
* not the outcome, so it is told not to.
|
|
30
|
+
*/
|
|
31
|
+
const DELAY_NOTE = 'The Bridge needs a moment to reconcile with Proton. Listing the label straight away ' +
|
|
32
|
+
'may not show the change yet, and an immediate check would be misleading rather than informative.';
|
|
33
|
+
const labelArgument = z
|
|
34
|
+
.string()
|
|
35
|
+
.describe(`The label name, for example "Work". The "${LABEL_PREFIX}" prefix may be included and is optional. ` +
|
|
36
|
+
'Use list_folders to see which labels exist. Labels are created in Proton itself, not here.');
|
|
37
|
+
const messageArgument = z
|
|
38
|
+
.string()
|
|
39
|
+
.describe('The message id from a listing or a search, with or without angle brackets.');
|
|
40
|
+
const mailboxHint = z
|
|
41
|
+
.string()
|
|
42
|
+
.optional()
|
|
43
|
+
.describe('The mailbox the message is in, if known. Only a shortcut for the lookup.');
|
|
44
|
+
export function registerLabelTools(server, connection, readOnly) {
|
|
45
|
+
server.registerTool('add_label', {
|
|
46
|
+
title: 'Apply a label',
|
|
47
|
+
description: 'Applies an existing label to a message. In Proton a message lives in exactly one folder ' +
|
|
48
|
+
'but can carry any number of labels, so this adds to the message rather than moving it: ' +
|
|
49
|
+
'it stays where it is and keeps the labels it already has. ' +
|
|
50
|
+
'The label has to exist already; this server does not create labels.',
|
|
51
|
+
inputSchema: z.object({
|
|
52
|
+
messageId: messageArgument,
|
|
53
|
+
label: labelArgument,
|
|
54
|
+
mailbox: mailboxHint,
|
|
55
|
+
}),
|
|
56
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
57
|
+
}, async ({ messageId, label, mailbox }, ctx) => track(() => withSignIn(ctx, async () => {
|
|
58
|
+
try {
|
|
59
|
+
const result = await addLabel(connection, readOnly(), messageId, label, mailbox);
|
|
60
|
+
return ok(`Applied "${result.label}" to ${result.messageId}, which is in "${result.path}". ` +
|
|
61
|
+
`It has not been moved. ${DELAY_NOTE}`);
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
return describeFailure(error, `applying the label "${label}"`);
|
|
65
|
+
}
|
|
66
|
+
}), 'add_label'));
|
|
67
|
+
server.registerTool('remove_label', {
|
|
68
|
+
title: 'Remove a label',
|
|
69
|
+
description: 'Removes a label from a message. The message keeps its folder and its other labels, and ' +
|
|
70
|
+
'does not go to trash. Only labels can be removed this way, never folders: a message ' +
|
|
71
|
+
'always lives in some folder, and moving it is a different operation.',
|
|
72
|
+
inputSchema: z.object({
|
|
73
|
+
messageId: messageArgument,
|
|
74
|
+
label: labelArgument,
|
|
75
|
+
}),
|
|
76
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
77
|
+
}, async ({ messageId, label }, ctx) => track(() => withSignIn(ctx, async () => {
|
|
78
|
+
try {
|
|
79
|
+
const result = await removeLabel(connection, readOnly(), messageId, label);
|
|
80
|
+
if (!result.wasApplied) {
|
|
81
|
+
return ok(`${result.messageId} does not carry "${result.label}", so there was nothing to remove. ` +
|
|
82
|
+
'Nothing was changed.');
|
|
83
|
+
}
|
|
84
|
+
return ok(`Removed "${result.label}" from ${result.messageId}. The message keeps its folder and ` +
|
|
85
|
+
`its other labels. ${DELAY_NOTE}`);
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
return describeFailure(error, `removing the label "${label}"`);
|
|
89
|
+
}
|
|
90
|
+
}), 'remove_label'));
|
|
91
|
+
}
|
|
92
|
+
//# sourceMappingURL=labels.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"labels.js","sourceRoot":"","sources":["../../src/tools/labels.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool: list_folders
|
|
3
|
+
*
|
|
4
|
+
* Lists the mailboxes. Deliberately its own tool and never a side dish of
|
|
5
|
+
* another answer: mailbox names are personal data on their own. Folders and
|
|
6
|
+
* labels belong to the Proton account rather than to a single address, so this
|
|
7
|
+
* exposes the whole account structure. A name like Folders/Banking/Revolut
|
|
8
|
+
* gives away a bank even when not a single message inside it is visible.
|
|
9
|
+
*/
|
|
10
|
+
import type { McpServer } from '@modelcontextprotocol/server';
|
|
11
|
+
import type { Connection, Mailbox } from '../bridge/connection.js';
|
|
12
|
+
/** Builds the text output. Kept compact, because mailbox lists get long. */
|
|
13
|
+
export declare function format(mailboxes: Mailbox[]): string;
|
|
14
|
+
export declare function registerListFolders(server: McpServer, connection: Connection): void;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool: list_folders
|
|
3
|
+
*
|
|
4
|
+
* Lists the mailboxes. Deliberately its own tool and never a side dish of
|
|
5
|
+
* another answer: mailbox names are personal data on their own. Folders and
|
|
6
|
+
* labels belong to the Proton account rather than to a single address, so this
|
|
7
|
+
* exposes the whole account structure. A name like Folders/Banking/Revolut
|
|
8
|
+
* gives away a bank even when not a single message inside it is visible.
|
|
9
|
+
*/
|
|
10
|
+
import * as z from 'zod';
|
|
11
|
+
import { describeFailure, withSignIn } from './failures.js';
|
|
12
|
+
import { track } from '../in-flight.js';
|
|
13
|
+
const inputSchema = z.object({
|
|
14
|
+
kind: z
|
|
15
|
+
.enum(['all', 'system', 'folder', 'label'])
|
|
16
|
+
.default('all')
|
|
17
|
+
.describe('Which mailboxes to list. "system" covers Inbox, Sent, Trash and so on, ' +
|
|
18
|
+
'"folder" the user-created folders, "label" the labels.'),
|
|
19
|
+
onlySelectable: z
|
|
20
|
+
.boolean()
|
|
21
|
+
.default(false)
|
|
22
|
+
.describe('When true, only mailboxes that can hold messages are listed.'),
|
|
23
|
+
});
|
|
24
|
+
/** Builds the text output. Kept compact, because mailbox lists get long. */
|
|
25
|
+
export function format(mailboxes) {
|
|
26
|
+
if (mailboxes.length === 0)
|
|
27
|
+
return 'No mailboxes found.';
|
|
28
|
+
const groups = [
|
|
29
|
+
['system', 'System mailboxes'],
|
|
30
|
+
['folder', 'Folders'],
|
|
31
|
+
['label', 'Labels'],
|
|
32
|
+
];
|
|
33
|
+
const lines = [];
|
|
34
|
+
for (const [kind, heading] of groups) {
|
|
35
|
+
const part = mailboxes.filter((m) => m.kind === kind && m.selectable);
|
|
36
|
+
if (part.length === 0)
|
|
37
|
+
continue;
|
|
38
|
+
lines.push(`## ${heading} (${part.length})`);
|
|
39
|
+
for (const m of part) {
|
|
40
|
+
const role = m.specialUse ? ` [${m.specialUse}]` : '';
|
|
41
|
+
lines.push(`- ${m.path}${role}`);
|
|
42
|
+
}
|
|
43
|
+
lines.push('');
|
|
44
|
+
}
|
|
45
|
+
return lines.join('\n').trimEnd();
|
|
46
|
+
}
|
|
47
|
+
export function registerListFolders(server, connection) {
|
|
48
|
+
server.registerTool('list_folders', {
|
|
49
|
+
title: 'List mailboxes',
|
|
50
|
+
description: 'Lists the folders and labels of the Proton account. ' +
|
|
51
|
+
'Proton distinguishes folders (a message lives in exactly one) from labels ' +
|
|
52
|
+
'(a message can carry any number). Over IMAP both appear as directories.',
|
|
53
|
+
inputSchema,
|
|
54
|
+
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
55
|
+
}, async ({ kind, onlySelectable }, ctx) => track(() => withSignIn(ctx, async () => {
|
|
56
|
+
try {
|
|
57
|
+
let mailboxes = await connection.listMailboxes();
|
|
58
|
+
if (kind !== 'all')
|
|
59
|
+
mailboxes = mailboxes.filter((m) => m.kind === kind);
|
|
60
|
+
if (onlySelectable)
|
|
61
|
+
mailboxes = mailboxes.filter((m) => m.selectable);
|
|
62
|
+
const answer = { content: [{ type: 'text', text: format(mailboxes) }] };
|
|
63
|
+
return answer;
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
return describeFailure(error, 'listing mailboxes');
|
|
67
|
+
}
|
|
68
|
+
}), 'list_folders'));
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=list-folders.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"list-folders.js","sourceRoot":"","sources":["../../src/tools/list-folders.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The reading tools: list_messages, get_message, get_attachment.
|
|
3
|
+
*
|
|
4
|
+
* All three are read-only. Every one of them goes through track() so that a
|
|
5
|
+
* shutdown waits for the answer instead of cutting it off.
|
|
6
|
+
*/
|
|
7
|
+
import type { McpServer } from '@modelcontextprotocol/server';
|
|
8
|
+
import type { Connection } from '../bridge/connection.js';
|
|
9
|
+
export declare function registerMessageTools(server: McpServer, connection: Connection): void;
|