@chrischall/office-outlook-mcp 0.6.1 → 0.7.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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +78 -8
- package/dist/auth-fetchproxy.js +26 -122
- package/dist/bundle.js +2346 -396
- package/dist/client.js +45 -84
- package/dist/index.js +2 -0
- package/dist/token-cache.js +2 -11
- package/dist/tools/_invite.js +1 -1
- package/dist/tools/export.js +1041 -0
- package/dist/tools/mail.js +4 -4
- package/dist/tools/writes.js +12 -22
- package/dist/version.js +1 -1
- package/mint.yaml +7 -0
- package/package.json +6 -2
- package/server.json +9 -3
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
},
|
|
8
8
|
"metadata": {
|
|
9
9
|
"description": "MCP server for Outlook / Microsoft 365 — mail, calendar, contacts and tasks via natural language",
|
|
10
|
-
"version": "0.
|
|
10
|
+
"version": "0.7.0"
|
|
11
11
|
},
|
|
12
12
|
"plugins": [
|
|
13
13
|
{
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"displayName": "Outlook (Microsoft 365)",
|
|
16
16
|
"source": "./",
|
|
17
17
|
"description": "MCP server for Outlook / Microsoft 365 — triage unread mail, reply, answer meeting invites and schedule meetings, plus folders, calendar, contacts and tasks, with confirmation before anything is sent",
|
|
18
|
-
"version": "0.
|
|
18
|
+
"version": "0.7.0",
|
|
19
19
|
"author": {
|
|
20
20
|
"name": "Chris Hall"
|
|
21
21
|
},
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "office-outlook-mcp",
|
|
3
3
|
"displayName": "Outlook (Microsoft 365)",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.7.0",
|
|
5
5
|
"description": "Outlook / Microsoft 365 for Claude — triage unread mail, reply, answer meeting invites and schedule meetings, plus folders, calendar, contacts and tasks, with confirmation before anything is sent, using your signed-in browser session",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Chris Hall",
|
package/README.md
CHANGED
|
@@ -103,11 +103,12 @@ Everything is optional — with nothing set, the server captures from the browse
|
|
|
103
103
|
| `OUTLOOK_CAPTURE_TIMEOUT` | Seconds to wait for the tab to make a readable request. Bounds the WHOLE capture, both declared hosts included. Defaults to `30`. |
|
|
104
104
|
| `OUTLOOK_TOKEN_CACHE` | `false` to stop caching the token between runs. |
|
|
105
105
|
| `OUTLOOK_TOKEN_FILE` | Path to the cached token. Defaults to `~/.office-outlook-mcp/token.json`. |
|
|
106
|
+
| `OUTLOOK_OUTPUT_DIR` | Absolute directory `outlook_export_messages` and `outlook_export_events` write into. Defaults to `~/Downloads/office-outlook-mcp`. When set, a per-call `dest` must be inside it. See [Bulk export](#bulk-export). |
|
|
106
107
|
|
|
107
108
|
## Tools
|
|
108
109
|
|
|
109
110
|
**Read** — `outlook_list_folders`, `outlook_list_messages`,
|
|
110
|
-
`outlook_get_message`, `
|
|
111
|
+
`outlook_get_message`, `outlook_list_unread_messages`, `outlook_list_attachments`,
|
|
111
112
|
`outlook_get_attachment`, `outlook_list_events`, `outlook_get_event`,
|
|
112
113
|
`outlook_list_calendars`, `outlook_find_meeting_times`,
|
|
113
114
|
`outlook_get_schedule`, `outlook_get_profile`,
|
|
@@ -115,7 +116,7 @@ Everything is optional — with nothing set, the server captures from the browse
|
|
|
115
116
|
`outlook_list_categories`, `outlook_list_tasks`
|
|
116
117
|
|
|
117
118
|
**Write** (ask you to confirm first — see [Confirmations](#confirmations)) —
|
|
118
|
-
`
|
|
119
|
+
`outlook_send_message`, `outlook_create_draft`, `outlook_mark_read`,
|
|
119
120
|
`outlook_update_message`, `outlook_move_message`, `outlook_create_event`,
|
|
120
121
|
`outlook_update_event`, `outlook_respond_to_invite`, `outlook_delete_event`,
|
|
121
122
|
`outlook_reply`.
|
|
@@ -146,6 +147,10 @@ what that decision needs in every view: `MyResponse`, `ResponseRequested`,
|
|
|
146
147
|
`IsOrganizer`, `IsCancelled`, `Type` (SingleInstance, Occurrence, Exception or
|
|
147
148
|
SeriesMaster), `ShowAs` and `IsAllDay`, plus `SeriesMasterId` in the full view.
|
|
148
149
|
|
|
150
|
+
**Export** — `outlook_export_messages` (mail) and `outlook_export_events`
|
|
151
|
+
(calendar) write JSONL or Markdown files, optionally with attachments, and
|
|
152
|
+
return only where they are; see [Bulk export](#bulk-export).
|
|
153
|
+
|
|
149
154
|
**Diagnostics** — `outlook_healthcheck`
|
|
150
155
|
|
|
151
156
|
Every read tool takes `view: compact | full | raw`, defaulting to **compact**.
|
|
@@ -156,9 +161,10 @@ mailbox time zone, so its preview can name the zone it would book in; that is
|
|
|
156
161
|
the one read a preview makes.)
|
|
157
162
|
|
|
158
163
|
Mail and event text is written by other people, so `outlook_list_messages`,
|
|
159
|
-
`outlook_get_message`, `
|
|
160
|
-
`outlook_list_events`, `outlook_get_event`, `outlook_find_meeting_times
|
|
161
|
-
`outlook_get_schedule`
|
|
164
|
+
`outlook_get_message`, `outlook_list_unread_messages`, `outlook_get_attachment`,
|
|
165
|
+
`outlook_list_events`, `outlook_get_event`, `outlook_find_meeting_times`,
|
|
166
|
+
`outlook_get_schedule`, `outlook_export_messages` and `outlook_export_events`
|
|
167
|
+
wrap every result (all views) in an untrusted-content
|
|
162
168
|
envelope — `untrusted_content: true` plus a `note` telling the model to treat
|
|
163
169
|
the text as data, never instructions (a `view: raw` record that carries its
|
|
164
170
|
own `untrusted_content`/`note` key is nested under `data`, so it cannot
|
|
@@ -173,7 +179,7 @@ before running non-read-only tools — as the real guard on outbound sends.
|
|
|
173
179
|
|
|
174
180
|
An agent can work through new mail end to end without opening Outlook:
|
|
175
181
|
|
|
176
|
-
1. **Read the batch** — `
|
|
182
|
+
1. **Read the batch** — `outlook_list_unread_messages` returns the unread messages in a
|
|
177
183
|
folder (default inbox), newest first, each with its plain-text body, in one
|
|
178
184
|
call. It does **not** mark anything read. Each item has a `kind`: `mail`,
|
|
179
185
|
`meetingRequest`, `meetingCancelled` or `meetingResponse`; requests and
|
|
@@ -198,7 +204,7 @@ An agent can work through new mail end to end without opening Outlook:
|
|
|
198
204
|
`outlook_move_message` moves one to another folder.
|
|
199
205
|
4. **Mark it handled** — `outlook_mark_read` (or `isRead: true` on
|
|
200
206
|
`outlook_update_message`) on the plain mail you handled, so the next
|
|
201
|
-
`
|
|
207
|
+
`outlook_list_unread_messages` returns only what is still new. Answered invites are
|
|
202
208
|
already out of the inbox (see step 2).
|
|
203
209
|
|
|
204
210
|
Replies, forwards, invite responses and new meetings reach other people, so
|
|
@@ -208,7 +214,7 @@ each asks you to confirm first — except a reply saved with `draftOnly: true`
|
|
|
208
214
|
|
|
209
215
|
## Paging from a time
|
|
210
216
|
|
|
211
|
-
`
|
|
217
|
+
`outlook_list_unread_messages` and `outlook_list_messages` also page **oldest first**
|
|
212
218
|
from a point in time: pass `from` as an ISO 8601 UTC time
|
|
213
219
|
(`2026-10-01T00:00:00Z`). The result carries `next_from` — pass it back
|
|
214
220
|
unchanged as `from` for the next page — and `has_more`. To backfill, start
|
|
@@ -218,6 +224,70 @@ returns the same position). Messages that share a timestamp are never skipped
|
|
|
218
224
|
or repeated across pages, and marking mail read between pages loses nothing.
|
|
219
225
|
`from` cannot be combined with `nextLink`, `sinceHours`, `search` or `skip`.
|
|
220
226
|
|
|
227
|
+
## Bulk export
|
|
228
|
+
|
|
229
|
+
`outlook_export_messages` exports mail and `outlook_export_events` exports
|
|
230
|
+
the calendar in bulk, thousands of records at a time, and none of their text
|
|
231
|
+
enters the conversation. The server pages the mailbox (or calendar) and writes
|
|
232
|
+
the records to files. The tool returns only a small manifest: where the files
|
|
233
|
+
are, their sizes, the record and attachment counts, the date span, and
|
|
234
|
+
`has_more` plus a `cursor`. Have the agent read the files with shell tools
|
|
235
|
+
(`jq`, `grep`, `rg`, `head`) rather than opening them whole.
|
|
236
|
+
|
|
237
|
+
- **Mail arguments:** `folder` (a well-known name such as `inbox` or
|
|
238
|
+
`sentitems`, or a folder id; `all` exports the whole mailbox; default
|
|
239
|
+
`inbox`), `since` and `until` (ISO 8601), `search` (full-text, spans the
|
|
240
|
+
mailbox). Mail is exported oldest first.
|
|
241
|
+
- **Event arguments:** `since` and `until` (ISO 8601, both required, at most
|
|
242
|
+
two years apart) and `calendar` (a calendar id; default, the default
|
|
243
|
+
calendar). Events come from the calendar view, so a recurring series is
|
|
244
|
+
expanded into its individual occurrences. Times are wall-clock times in the
|
|
245
|
+
mailbox's time zone, named on each record as `time_zone`.
|
|
246
|
+
- **Both:** `format` (`jsonl`, the default, or `markdown`), `attachments`
|
|
247
|
+
(default `false`), `dest`, `cursor`, `max_items` (default 5000, max 50000)
|
|
248
|
+
and `budget_seconds` (default 90, max 600).
|
|
249
|
+
- **Files:** `part-0001.jsonl` holds one JSON record per message (id,
|
|
250
|
+
conversation id, subject, from, to/cc/bcc, dates, flags, categories, web link
|
|
251
|
+
and the full plain-text body). Its first line is an `_export` header that
|
|
252
|
+
names the source, account, arguments and an untrusted-content note.
|
|
253
|
+
An event record carries the id, iCal UID, type and series id, subject,
|
|
254
|
+
start, end, time zone, location, organizer, attendees with their responses,
|
|
255
|
+
your response, show-as, categories, the join link and the full plain-text
|
|
256
|
+
body.
|
|
257
|
+
`format: "markdown"` writes `part-0001.md`, a readable transcript (one
|
|
258
|
+
section per message or event), instead.
|
|
259
|
+
`manifest.json` lists the counts and any errors, and the files: on disk
|
|
260
|
+
every part's files (it is rewritten as each part finishes); in a blob zip
|
|
261
|
+
the files in that zip.
|
|
262
|
+
- **Attachments:** with `attachments: true`, each file attachment of up to
|
|
263
|
+
50 MB is saved as `attachments/<short id>/<name>` (the name sanitized), and
|
|
264
|
+
the record lists its attachments, each with its relative `path`, or with
|
|
265
|
+
`skipped` and the reason. Attached Outlook items (a forwarded email or
|
|
266
|
+
event), cloud-file links (OneDrive/SharePoint), larger files and failed
|
|
267
|
+
downloads are skipped. One bad attachment never fails the export.
|
|
268
|
+
- **Resuming:** each call stops at `max_items` or `budget_seconds`, whichever
|
|
269
|
+
comes first. While `has_more` is true, pass `cursor` back unchanged. The
|
|
270
|
+
next call continues the same export as `part-0002`, `part-0003` and so on.
|
|
271
|
+
With attachments on, the budget is checked between small groups of
|
|
272
|
+
records, so a stop can fall inside a page; the cursor resumes at the next
|
|
273
|
+
record. If Outlook refuses the credential (including while downloading an
|
|
274
|
+
attachment), or keeps returning 429 after three
|
|
275
|
+
Retry-After waits, the call keeps what it already wrote and returns the
|
|
276
|
+
cursor. Re-authenticate, then resume.
|
|
277
|
+
|
|
278
|
+
Where the files go depends on how the server runs:
|
|
279
|
+
|
|
280
|
+
| mode | when | `location` in the result |
|
|
281
|
+
| --- | --- | --- |
|
|
282
|
+
| `disk` | any local (stdio) install, the default | an **absolute local directory**, `<dir>/outlook-mail-<YYYYMMDD-HHmmss>/` (or `outlook-events-…`); each file entry carries its absolute `path` |
|
|
283
|
+
| `blob` | hosted on mcp-host, which sets both `MCP_BLOB_BASE_URL` and `MCP_BLOB_SIGNING_KEY` | a **signed download link** to one zip per call (`part-NNNN.zip`, which includes `manifest.json`), valid for **1 hour** (`url_expires_at`) |
|
|
284
|
+
|
|
285
|
+
On disk, `<dir>` is the per-call `dest`, else `OUTLOOK_OUTPUT_DIR`, else
|
|
286
|
+
`~/Downloads/office-outlook-mcp`. A `dest` must be inside `OUTLOOK_OUTPUT_DIR`
|
|
287
|
+
when that is set. Otherwise it must be inside `~/Downloads`, `~/Documents` or
|
|
288
|
+
`~/Desktop`. A resumed cursor's directory is checked against the same roots
|
|
289
|
+
again.
|
|
290
|
+
|
|
221
291
|
## Confirmations
|
|
222
292
|
|
|
223
293
|
Every write asks you to confirm before anything is sent or changed (except a
|
package/dist/auth-fetchproxy.js
CHANGED
|
@@ -24,9 +24,19 @@
|
|
|
24
24
|
* cannot drive. Capture is the only path, which is why a failed capture is a
|
|
25
25
|
* hard error with actionable guidance rather than a fallback.
|
|
26
26
|
*/
|
|
27
|
-
import {
|
|
27
|
+
import { captureBearerHeader } from '@chrischall/mcp-utils/fetchproxy';
|
|
28
28
|
import { readPortEnv, readTtlMsEnv } from '@chrischall/mcp-utils';
|
|
29
29
|
import { PACKAGE_NAME, VERSION } from './version.js';
|
|
30
|
+
/**
|
|
31
|
+
* `raceCaptures` settles the per-host attempts under ONE overall deadline:
|
|
32
|
+
* `Promise.any` alone is not enough, because the bridge serializes the two
|
|
33
|
+
* hosts behind its single connection, so the wait was the sum rather than the
|
|
34
|
+
* max — measured live at 14.4s for a 5s window and 36s for a 20s one. At the
|
|
35
|
+
* 30s default that lands past the 60s request timeout an MCP client uses by
|
|
36
|
+
* default. Re-exported (it is mcp-utils', lifted from here) for the test that
|
|
37
|
+
* pins that bound.
|
|
38
|
+
*/
|
|
39
|
+
export { raceCaptures, stripBearer } from '@chrischall/mcp-utils/fetchproxy';
|
|
30
40
|
/**
|
|
31
41
|
* The fetchproxy concentrator port.
|
|
32
42
|
*
|
|
@@ -87,81 +97,6 @@ export const TRUST_DOMAINS = [...CAPTURE_HOSTS];
|
|
|
87
97
|
* enough that an idle tab does not hang a tool call for minutes.
|
|
88
98
|
*/
|
|
89
99
|
const CAPTURE_TIMEOUT_MS = readTtlMsEnv('OUTLOOK_CAPTURE_TIMEOUT', 30_000);
|
|
90
|
-
/**
|
|
91
|
-
* The transport-wide deadline, derived from the capture window rather than left
|
|
92
|
-
* at the library default.
|
|
93
|
-
*
|
|
94
|
-
* `captureRequestHeader`'s own `timeoutMs` is silently capped by
|
|
95
|
-
* `fetchTimeoutMs`: a window raised past the deadline fails early at the
|
|
96
|
-
* deadline's number, which reads as a timeout nobody configured. Deriving it
|
|
97
|
-
* means the two cannot disagree.
|
|
98
|
-
*/
|
|
99
|
-
const BRIDGE_DEADLINE_MS = Math.max(CAPTURE_TIMEOUT_MS + 15_000, 30_000);
|
|
100
|
-
/**
|
|
101
|
-
* Shortest plausible token. Real ones are ~5 KB of JWT; this only guards
|
|
102
|
-
* against an empty or echoed header, not against a wrong-audience token.
|
|
103
|
-
*/
|
|
104
|
-
const MIN_TOKEN_LENGTH = 100;
|
|
105
|
-
/**
|
|
106
|
-
* Add `fetch` to the derived capability set.
|
|
107
|
-
*
|
|
108
|
-
* `createBootstrapOpts` infers capabilities from the bootstrap DECLARATIONS and
|
|
109
|
-
* the field REPLACES the server's defaults rather than extending them, so a
|
|
110
|
-
* declaration-only set can silently lose `fetch`.
|
|
111
|
-
*/
|
|
112
|
-
function withFetch(opts) {
|
|
113
|
-
const derived = opts.capabilities ?? [];
|
|
114
|
-
return {
|
|
115
|
-
...opts,
|
|
116
|
-
capabilities: derived.includes('fetch')
|
|
117
|
-
? derived
|
|
118
|
-
: [...derived, 'fetch'],
|
|
119
|
-
};
|
|
120
|
-
}
|
|
121
|
-
/**
|
|
122
|
-
* Settle a set of per-host capture attempts under ONE overall deadline.
|
|
123
|
-
*
|
|
124
|
-
* `Promise.any` alone is not enough. The two declared hosts are documented as
|
|
125
|
-
* raced, but the bridge serializes them behind its single connection, so the
|
|
126
|
-
* wait was the sum rather than the max — measured live at 14.4s for a 5s window
|
|
127
|
-
* and 36s for a 20s one. At the 30s default that lands past the 60s request
|
|
128
|
-
* timeout an MCP client uses by default, so the host gave up before this code
|
|
129
|
-
* could report the real reason. The window is a promise about total wait, and
|
|
130
|
-
* this is what keeps it.
|
|
131
|
-
*
|
|
132
|
-
* Exported for the test that pins that bound.
|
|
133
|
-
*/
|
|
134
|
-
export async function raceCaptures(attempts, windowMs) {
|
|
135
|
-
// A rejection that loses the race must not surface as an unhandled rejection
|
|
136
|
-
// once the winner has already settled the caller.
|
|
137
|
-
for (const a of attempts)
|
|
138
|
-
a.catch(() => { });
|
|
139
|
-
let timer;
|
|
140
|
-
const expiry = new Promise((_, reject) => {
|
|
141
|
-
timer = setTimeout(() => reject(new Error(`timed out after ${Math.round(windowMs / 1000)}s waiting for the page to ` +
|
|
142
|
-
'make a request we could read')), windowMs);
|
|
143
|
-
});
|
|
144
|
-
try {
|
|
145
|
-
// `any` resolves on the first SUCCESS rather than the first settle, so the
|
|
146
|
-
// host the user is not on failing does not sink the one they are on.
|
|
147
|
-
return await Promise.race([
|
|
148
|
-
Promise.any(attempts).catch((e) => {
|
|
149
|
-
throw new Error(e instanceof AggregateError
|
|
150
|
-
? e.errors.map((x) => x.message).join('; ')
|
|
151
|
-
: e.message);
|
|
152
|
-
}),
|
|
153
|
-
expiry,
|
|
154
|
-
]);
|
|
155
|
-
}
|
|
156
|
-
finally {
|
|
157
|
-
clearTimeout(timer);
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
/** Normalise a captured header to a bare token, dropping any `Bearer ` prefix. */
|
|
161
|
-
export function stripBearer(raw) {
|
|
162
|
-
const m = /^\s*Bearer\s+(.+)$/i.exec(raw);
|
|
163
|
-
return (m ? m[1] : raw).trim();
|
|
164
|
-
}
|
|
165
100
|
/**
|
|
166
101
|
* Capture an Outlook access token from the user's signed-in browser tab.
|
|
167
102
|
*
|
|
@@ -171,54 +106,23 @@ export function stripBearer(raw) {
|
|
|
171
106
|
* Returns the bare token (no `Bearer ` prefix).
|
|
172
107
|
*/
|
|
173
108
|
export async function captureTokenViaFetchproxy() {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
109
|
+
// `captureBearerHeader` declares only `capture_request_header` beside the
|
|
110
|
+
// `fetch` base verb, derives the bridge deadline from the window (window +
|
|
111
|
+
// 15 s, never under 30 s) so a raised window is not cut off early, starts the
|
|
112
|
+
// transport (skipping `start()` makes the first verb throw "ensureConnected
|
|
113
|
+
// called before listen()", which reads like a pairing problem), rejects a
|
|
114
|
+
// header too short to be a token (real ones are ~5 KB of JWT), and closes
|
|
115
|
+
// the transport whatever happened.
|
|
116
|
+
return captureBearerHeader({
|
|
117
|
+
captures: [CAPTURE_DECL_CLOUD, CAPTURE_DECL_OFFICE],
|
|
118
|
+
domains: TRUST_DOMAINS,
|
|
119
|
+
windowMs: CAPTURE_TIMEOUT_MS,
|
|
181
120
|
port: getWsPort(),
|
|
182
|
-
fetchTimeoutMs: BRIDGE_DEADLINE_MS,
|
|
183
121
|
serverName: PACKAGE_NAME,
|
|
184
122
|
version: VERSION,
|
|
123
|
+
what: 'an Outlook token',
|
|
124
|
+
guidance: 'Open a signed-in Outlook tab (outlook.cloud.microsoft or ' +
|
|
125
|
+
'outlook.office.com), leave it in the foreground for a moment so the ' +
|
|
126
|
+
'page makes a request, and retry.',
|
|
185
127
|
});
|
|
186
|
-
try {
|
|
187
|
-
// `start()` loads the identity keypair. It does not bind the port or dial —
|
|
188
|
-
// that stays lazy until the first verb — but skipping it makes the first
|
|
189
|
-
// verb throw "ensureConnected called before listen()", which reads exactly
|
|
190
|
-
// like a pairing problem and is not one.
|
|
191
|
-
await transport.start();
|
|
192
|
-
const attempts = [CAPTURE_DECL_CLOUD, CAPTURE_DECL_OFFICE].map((decl) => transport.server
|
|
193
|
-
.captureRequestHeader({ ...decl, timeoutMs: CAPTURE_TIMEOUT_MS })
|
|
194
|
-
.then((raw) => {
|
|
195
|
-
if (typeof raw !== 'string') {
|
|
196
|
-
throw new Error(`${decl.host}: capture returned no header`);
|
|
197
|
-
}
|
|
198
|
-
const token = stripBearer(raw);
|
|
199
|
-
if (token.length < MIN_TOKEN_LENGTH) {
|
|
200
|
-
throw new Error(`${decl.host}: authorization header was present but too short to be a ` +
|
|
201
|
-
`token (${token.length} chars)`);
|
|
202
|
-
}
|
|
203
|
-
return token;
|
|
204
|
-
}));
|
|
205
|
-
try {
|
|
206
|
-
return await raceCaptures(attempts, CAPTURE_TIMEOUT_MS);
|
|
207
|
-
}
|
|
208
|
-
catch (e) {
|
|
209
|
-
const detail = e.message;
|
|
210
|
-
throw new Error(`could not capture an Outlook token from the browser (${detail}). ` +
|
|
211
|
-
'Open a signed-in Outlook tab (outlook.cloud.microsoft or ' +
|
|
212
|
-
'outlook.office.com), leave it in the foreground for a moment so the ' +
|
|
213
|
-
'page makes a request, and retry.');
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
finally {
|
|
217
|
-
try {
|
|
218
|
-
await transport.close();
|
|
219
|
-
}
|
|
220
|
-
catch {
|
|
221
|
-
/* the token, or the original failure, is what matters */
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
128
|
}
|