mcp-twake-mail 0.1.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/LICENSE +663 -0
- package/README.md +332 -0
- package/build/auth/index.d.ts +3 -0
- package/build/auth/index.js +4 -0
- package/build/auth/index.js.map +1 -0
- package/build/auth/oidc-flow.d.ts +47 -0
- package/build/auth/oidc-flow.js +146 -0
- package/build/auth/oidc-flow.js.map +1 -0
- package/build/auth/token-refresh.d.ts +56 -0
- package/build/auth/token-refresh.js +132 -0
- package/build/auth/token-refresh.js.map +1 -0
- package/build/auth/token-store.d.ts +33 -0
- package/build/auth/token-store.js +63 -0
- package/build/auth/token-store.js.map +1 -0
- package/build/cli/commands/auth.d.ts +5 -0
- package/build/cli/commands/auth.js +49 -0
- package/build/cli/commands/auth.js.map +1 -0
- package/build/cli/commands/check.d.ts +4 -0
- package/build/cli/commands/check.js +125 -0
- package/build/cli/commands/check.js.map +1 -0
- package/build/cli/commands/setup.d.ts +4 -0
- package/build/cli/commands/setup.js +172 -0
- package/build/cli/commands/setup.js.map +1 -0
- package/build/cli/index.d.ts +15 -0
- package/build/cli/index.js +56 -0
- package/build/cli/index.js.map +1 -0
- package/build/cli/prompts/setup-wizard.d.ts +38 -0
- package/build/cli/prompts/setup-wizard.js +121 -0
- package/build/cli/prompts/setup-wizard.js.map +1 -0
- package/build/config/__tests__/logger.test.d.ts +1 -0
- package/build/config/__tests__/logger.test.js +28 -0
- package/build/config/__tests__/logger.test.js.map +1 -0
- package/build/config/__tests__/schema.test.d.ts +1 -0
- package/build/config/__tests__/schema.test.js +101 -0
- package/build/config/__tests__/schema.test.js.map +1 -0
- package/build/config/logger.d.ts +3 -0
- package/build/config/logger.js +9 -0
- package/build/config/logger.js.map +1 -0
- package/build/config/schema.d.ts +28 -0
- package/build/config/schema.js +81 -0
- package/build/config/schema.js.map +1 -0
- package/build/errors.d.ts +34 -0
- package/build/errors.js +154 -0
- package/build/errors.js.map +1 -0
- package/build/errors.test.d.ts +1 -0
- package/build/errors.test.js +234 -0
- package/build/errors.test.js.map +1 -0
- package/build/index.d.ts +2 -0
- package/build/index.js +12 -0
- package/build/index.js.map +1 -0
- package/build/jmap/client.d.ts +96 -0
- package/build/jmap/client.js +267 -0
- package/build/jmap/client.js.map +1 -0
- package/build/mcp/server.d.ts +24 -0
- package/build/mcp/server.js +68 -0
- package/build/mcp/server.js.map +1 -0
- package/build/mcp/tools/attachment.d.ts +30 -0
- package/build/mcp/tools/attachment.js +246 -0
- package/build/mcp/tools/attachment.js.map +1 -0
- package/build/mcp/tools/attachment.test.d.ts +1 -0
- package/build/mcp/tools/attachment.test.js +457 -0
- package/build/mcp/tools/attachment.test.js.map +1 -0
- package/build/mcp/tools/email-operations.d.ts +10 -0
- package/build/mcp/tools/email-operations.js +828 -0
- package/build/mcp/tools/email-operations.js.map +1 -0
- package/build/mcp/tools/email-operations.test.d.ts +1 -0
- package/build/mcp/tools/email-operations.test.js +453 -0
- package/build/mcp/tools/email-operations.test.js.map +1 -0
- package/build/mcp/tools/email-sending.d.ts +10 -0
- package/build/mcp/tools/email-sending.js +682 -0
- package/build/mcp/tools/email-sending.js.map +1 -0
- package/build/mcp/tools/email.d.ts +10 -0
- package/build/mcp/tools/email.js +365 -0
- package/build/mcp/tools/email.js.map +1 -0
- package/build/mcp/tools/email.test.d.ts +1 -0
- package/build/mcp/tools/email.test.js +332 -0
- package/build/mcp/tools/email.test.js.map +1 -0
- package/build/mcp/tools/index.d.ts +14 -0
- package/build/mcp/tools/index.js +29 -0
- package/build/mcp/tools/index.js.map +1 -0
- package/build/mcp/tools/mailbox.d.ts +10 -0
- package/build/mcp/tools/mailbox.js +195 -0
- package/build/mcp/tools/mailbox.js.map +1 -0
- package/build/mcp/tools/mailbox.test.d.ts +1 -0
- package/build/mcp/tools/mailbox.test.js +231 -0
- package/build/mcp/tools/mailbox.test.js.map +1 -0
- package/build/mcp/tools/thread.d.ts +10 -0
- package/build/mcp/tools/thread.js +282 -0
- package/build/mcp/tools/thread.js.map +1 -0
- package/build/mcp/tools/thread.test.d.ts +1 -0
- package/build/mcp/tools/thread.test.js +384 -0
- package/build/mcp/tools/thread.test.js.map +1 -0
- package/build/transformers/__tests__/email.test.d.ts +1 -0
- package/build/transformers/__tests__/email.test.js +438 -0
- package/build/transformers/__tests__/email.test.js.map +1 -0
- package/build/transformers/__tests__/mailbox.test.d.ts +1 -0
- package/build/transformers/__tests__/mailbox.test.js +222 -0
- package/build/transformers/__tests__/mailbox.test.js.map +1 -0
- package/build/transformers/email.d.ts +76 -0
- package/build/transformers/email.js +138 -0
- package/build/transformers/email.js.map +1 -0
- package/build/transformers/index.d.ts +5 -0
- package/build/transformers/index.js +6 -0
- package/build/transformers/index.js.map +1 -0
- package/build/transformers/mailbox.d.ts +43 -0
- package/build/transformers/mailbox.js +70 -0
- package/build/transformers/mailbox.js.map +1 -0
- package/build/types/dto.d.ts +91 -0
- package/build/types/dto.js +9 -0
- package/build/types/dto.js.map +1 -0
- package/build/types/jmap.d.ts +110 -0
- package/build/types/jmap.js +5 -0
- package/build/types/jmap.js.map +1 -0
- package/package.json +71 -0
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { JMAPError } from '../errors.js';
|
|
2
|
+
import { createTokenRefresher } from '../auth/token-refresh.js';
|
|
3
|
+
/** Default JMAP capabilities for mail operations */
|
|
4
|
+
const DEFAULT_USING = ['urn:ietf:params:jmap:core', 'urn:ietf:params:jmap:mail'];
|
|
5
|
+
/** Session fetch timeout (quick check) */
|
|
6
|
+
const SESSION_TIMEOUT = 5000;
|
|
7
|
+
/**
|
|
8
|
+
* JMAP client for interacting with JMAP mail servers.
|
|
9
|
+
* Handles authentication, session management, and request batching.
|
|
10
|
+
*/
|
|
11
|
+
export class JMAPClient {
|
|
12
|
+
session = null;
|
|
13
|
+
config;
|
|
14
|
+
logger;
|
|
15
|
+
stateTracker = new Map();
|
|
16
|
+
tokenRefresher = null;
|
|
17
|
+
constructor(config, logger) {
|
|
18
|
+
this.config = config;
|
|
19
|
+
this.logger = logger;
|
|
20
|
+
// Initialize TokenRefresher for OIDC authentication
|
|
21
|
+
if (config.JMAP_AUTH_METHOD === 'oidc' && config.JMAP_OIDC_ISSUER && config.JMAP_OIDC_CLIENT_ID) {
|
|
22
|
+
this.tokenRefresher = createTokenRefresher(config.JMAP_OIDC_ISSUER, config.JMAP_OIDC_CLIENT_ID);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Generate authentication headers based on configured auth method.
|
|
27
|
+
* For OIDC, automatically refreshes tokens if needed via TokenRefresher.
|
|
28
|
+
* @returns Headers object with Authorization and Content-Type
|
|
29
|
+
*/
|
|
30
|
+
async getAuthHeaders() {
|
|
31
|
+
const headers = {
|
|
32
|
+
'Content-Type': 'application/json',
|
|
33
|
+
};
|
|
34
|
+
if (this.config.JMAP_AUTH_METHOD === 'basic') {
|
|
35
|
+
const credentials = `${this.config.JMAP_USERNAME}:${this.config.JMAP_PASSWORD}`;
|
|
36
|
+
const token = Buffer.from(credentials).toString('base64');
|
|
37
|
+
headers['Authorization'] = `Basic ${token}`;
|
|
38
|
+
}
|
|
39
|
+
else if (this.config.JMAP_AUTH_METHOD === 'oidc') {
|
|
40
|
+
// OIDC: use TokenRefresher to get valid token (auto-refreshes if needed)
|
|
41
|
+
if (!this.tokenRefresher) {
|
|
42
|
+
throw new JMAPError('OIDC configuration incomplete', 'oidcConfigError', 'Ensure OIDC_ISSUER and OIDC_CLIENT_ID are configured.');
|
|
43
|
+
}
|
|
44
|
+
const tokens = await this.tokenRefresher.ensureValidToken();
|
|
45
|
+
headers['Authorization'] = `Bearer ${tokens.accessToken}`;
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
// Bearer: use static token from config
|
|
49
|
+
headers['Authorization'] = `Bearer ${this.config.JMAP_TOKEN}`;
|
|
50
|
+
}
|
|
51
|
+
return headers;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Fetch JMAP session from the server.
|
|
55
|
+
* Discovers apiUrl, accountId, and capabilities.
|
|
56
|
+
* @returns JMAPSession with extracted session data
|
|
57
|
+
* @throws JMAPError if session fetch fails or no mail account found
|
|
58
|
+
*/
|
|
59
|
+
async fetchSession() {
|
|
60
|
+
this.logger.info({ url: this.config.JMAP_SESSION_URL }, 'Fetching JMAP session...');
|
|
61
|
+
let response;
|
|
62
|
+
try {
|
|
63
|
+
response = await fetch(this.config.JMAP_SESSION_URL, {
|
|
64
|
+
method: 'GET',
|
|
65
|
+
headers: await this.getAuthHeaders(),
|
|
66
|
+
signal: AbortSignal.timeout(SESSION_TIMEOUT),
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
if (error instanceof Error && (error.name === 'TimeoutError' || error.name === 'AbortError')) {
|
|
71
|
+
throw JMAPError.timeout('session fetch');
|
|
72
|
+
}
|
|
73
|
+
throw error;
|
|
74
|
+
}
|
|
75
|
+
if (!response.ok) {
|
|
76
|
+
throw JMAPError.httpError(response.status, response.statusText);
|
|
77
|
+
}
|
|
78
|
+
const sessionData = (await response.json());
|
|
79
|
+
// Extract primary account for mail capability
|
|
80
|
+
const accountId = sessionData.primaryAccounts['urn:ietf:params:jmap:mail'];
|
|
81
|
+
if (!accountId) {
|
|
82
|
+
throw JMAPError.methodError('noMailAccount', 'No mail account found in JMAP session. The server may not support JMAP Mail.');
|
|
83
|
+
}
|
|
84
|
+
// Validate downloadUrl exists (required for blob downloads)
|
|
85
|
+
if (!sessionData.downloadUrl) {
|
|
86
|
+
this.logger.warn('No downloadUrl in JMAP session - attachment downloads will not work');
|
|
87
|
+
}
|
|
88
|
+
this.session = {
|
|
89
|
+
apiUrl: sessionData.apiUrl,
|
|
90
|
+
downloadUrl: sessionData.downloadUrl || '',
|
|
91
|
+
accountId,
|
|
92
|
+
capabilities: sessionData.capabilities,
|
|
93
|
+
state: sessionData.state,
|
|
94
|
+
};
|
|
95
|
+
this.logger.info({ accountId, apiUrl: sessionData.apiUrl }, 'JMAP session established');
|
|
96
|
+
return this.session;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Get the current session.
|
|
100
|
+
* @returns JMAPSession
|
|
101
|
+
* @throws JMAPError if session not initialized
|
|
102
|
+
*/
|
|
103
|
+
getSession() {
|
|
104
|
+
if (!this.session) {
|
|
105
|
+
throw new JMAPError('Session not initialized', 'sessionNotInitialized', 'Call fetchSession() before making requests.');
|
|
106
|
+
}
|
|
107
|
+
return this.session;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Make a batched JMAP request with multiple method calls.
|
|
111
|
+
* @param methodCalls Array of method calls to execute
|
|
112
|
+
* @param using Optional array of capability URIs (defaults to core + mail)
|
|
113
|
+
* @returns JMAPResponse with method responses
|
|
114
|
+
* @throws JMAPError on HTTP errors or timeouts
|
|
115
|
+
*/
|
|
116
|
+
async request(methodCalls, using = DEFAULT_USING) {
|
|
117
|
+
const session = this.getSession();
|
|
118
|
+
const requestBody = {
|
|
119
|
+
using,
|
|
120
|
+
methodCalls,
|
|
121
|
+
};
|
|
122
|
+
this.logger.debug({ methodCount: methodCalls.length, methods: methodCalls.map((mc) => mc[0]) }, 'Sending JMAP request');
|
|
123
|
+
let response;
|
|
124
|
+
try {
|
|
125
|
+
response = await fetch(session.apiUrl, {
|
|
126
|
+
method: 'POST',
|
|
127
|
+
headers: await this.getAuthHeaders(),
|
|
128
|
+
body: JSON.stringify(requestBody),
|
|
129
|
+
signal: AbortSignal.timeout(this.config.JMAP_REQUEST_TIMEOUT),
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
if (error instanceof Error && (error.name === 'TimeoutError' || error.name === 'AbortError')) {
|
|
134
|
+
throw JMAPError.timeout('JMAP request');
|
|
135
|
+
}
|
|
136
|
+
throw error;
|
|
137
|
+
}
|
|
138
|
+
if (!response.ok) {
|
|
139
|
+
throw JMAPError.httpError(response.status, response.statusText);
|
|
140
|
+
}
|
|
141
|
+
const jmapResponse = (await response.json());
|
|
142
|
+
// Check for session state changes
|
|
143
|
+
if (jmapResponse.sessionState && jmapResponse.sessionState !== this.session?.state) {
|
|
144
|
+
this.logger.warn({ oldState: this.session?.state, newState: jmapResponse.sessionState }, 'Session state changed - session refresh may be needed');
|
|
145
|
+
}
|
|
146
|
+
// Update state tracking from method responses
|
|
147
|
+
for (const methodResponse of jmapResponse.methodResponses) {
|
|
148
|
+
this.extractAndUpdateState(methodResponse);
|
|
149
|
+
}
|
|
150
|
+
return jmapResponse;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Parse a method response and extract success/error status.
|
|
154
|
+
* @param response The method response tuple
|
|
155
|
+
* @returns Object with success flag and data or error
|
|
156
|
+
*/
|
|
157
|
+
parseMethodResponse(response) {
|
|
158
|
+
const [methodName, responseData] = response;
|
|
159
|
+
// Check if this is an error response
|
|
160
|
+
if (methodName === 'error') {
|
|
161
|
+
const errorResponse = {
|
|
162
|
+
type: responseData.type || 'unknownError',
|
|
163
|
+
description: responseData.description,
|
|
164
|
+
};
|
|
165
|
+
this.logger.debug({ error: errorResponse }, 'JMAP method error');
|
|
166
|
+
return {
|
|
167
|
+
success: false,
|
|
168
|
+
error: errorResponse,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
return {
|
|
172
|
+
success: true,
|
|
173
|
+
data: responseData,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Extract state from method response and update tracker.
|
|
178
|
+
* @param response The method response tuple
|
|
179
|
+
*/
|
|
180
|
+
extractAndUpdateState(response) {
|
|
181
|
+
const [methodName, responseData] = response;
|
|
182
|
+
// Skip error responses
|
|
183
|
+
if (methodName === 'error') {
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
// Extract object type from method name (e.g., 'Email/get' -> 'Email')
|
|
187
|
+
const objectType = methodName.split('/')[0];
|
|
188
|
+
// Check for state or newState in response
|
|
189
|
+
const state = responseData.newState || responseData.state;
|
|
190
|
+
if (state && objectType) {
|
|
191
|
+
this.updateState(objectType, state);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Update tracked state for an object type.
|
|
196
|
+
* @param type Object type (e.g., 'Email', 'Mailbox')
|
|
197
|
+
* @param state New state string
|
|
198
|
+
*/
|
|
199
|
+
updateState(type, state) {
|
|
200
|
+
this.stateTracker.set(type, state);
|
|
201
|
+
this.logger.debug({ type, state }, 'State updated');
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Get tracked state for an object type.
|
|
205
|
+
* @param type Object type (e.g., 'Email', 'Mailbox')
|
|
206
|
+
* @returns State string or undefined if not tracked
|
|
207
|
+
*/
|
|
208
|
+
getState(type) {
|
|
209
|
+
return this.stateTracker.get(type);
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Clear tracked state.
|
|
213
|
+
* @param type Optional object type to clear. If not provided, clears all state.
|
|
214
|
+
*/
|
|
215
|
+
clearState(type) {
|
|
216
|
+
if (type) {
|
|
217
|
+
this.stateTracker.delete(type);
|
|
218
|
+
this.logger.debug({ type }, 'State cleared');
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
this.stateTracker.clear();
|
|
222
|
+
this.logger.debug('All state cleared');
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Download a blob (attachment) from the JMAP server.
|
|
227
|
+
* Uses the downloadUrl template from the session.
|
|
228
|
+
* @param blobId The blob ID to download
|
|
229
|
+
* @param name Optional filename for the download
|
|
230
|
+
* @param type Optional MIME type
|
|
231
|
+
* @returns ArrayBuffer containing the blob data
|
|
232
|
+
* @throws JMAPError if download fails
|
|
233
|
+
*/
|
|
234
|
+
async downloadBlob(blobId, name, type) {
|
|
235
|
+
const session = this.getSession();
|
|
236
|
+
if (!session.downloadUrl) {
|
|
237
|
+
throw new JMAPError('Download URL not available', 'downloadUrlMissing', 'The JMAP server did not provide a download URL in the session.');
|
|
238
|
+
}
|
|
239
|
+
// Build download URL from template
|
|
240
|
+
// Template format: {downloadUrl}/{accountId}/{blobId}/{name}?type={type}
|
|
241
|
+
let url = session.downloadUrl
|
|
242
|
+
.replace('{accountId}', encodeURIComponent(session.accountId))
|
|
243
|
+
.replace('{blobId}', encodeURIComponent(blobId))
|
|
244
|
+
.replace('{name}', encodeURIComponent(name || 'attachment'))
|
|
245
|
+
.replace('{type}', encodeURIComponent(type || 'application/octet-stream'));
|
|
246
|
+
this.logger.debug({ blobId, name, type, url }, 'Downloading blob');
|
|
247
|
+
let response;
|
|
248
|
+
try {
|
|
249
|
+
response = await fetch(url, {
|
|
250
|
+
method: 'GET',
|
|
251
|
+
headers: await this.getAuthHeaders(),
|
|
252
|
+
signal: AbortSignal.timeout(this.config.JMAP_REQUEST_TIMEOUT),
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
catch (error) {
|
|
256
|
+
if (error instanceof Error && (error.name === 'TimeoutError' || error.name === 'AbortError')) {
|
|
257
|
+
throw JMAPError.timeout('blob download');
|
|
258
|
+
}
|
|
259
|
+
throw error;
|
|
260
|
+
}
|
|
261
|
+
if (!response.ok) {
|
|
262
|
+
throw JMAPError.httpError(response.status, response.statusText);
|
|
263
|
+
}
|
|
264
|
+
return response.arrayBuffer();
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/jmap/client.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP server for JMAP mail operations.
|
|
3
|
+
* Uses stdio transport for AI assistant communication.
|
|
4
|
+
*/
|
|
5
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
6
|
+
import { type Config } from '../config/schema.js';
|
|
7
|
+
import { type Logger } from '../config/logger.js';
|
|
8
|
+
import { JMAPClient } from '../jmap/client.js';
|
|
9
|
+
/**
|
|
10
|
+
* Create MCP server instance with JMAP client.
|
|
11
|
+
* @param config Validated configuration
|
|
12
|
+
* @param logger Pino logger (stderr only)
|
|
13
|
+
* @returns Object with server and jmapClient
|
|
14
|
+
*/
|
|
15
|
+
export declare function createMCPServer(config: Config, logger: Logger): {
|
|
16
|
+
server: McpServer;
|
|
17
|
+
jmapClient: JMAPClient;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Start the MCP server with JMAP validation.
|
|
21
|
+
* Validates JMAP connection before accepting MCP requests.
|
|
22
|
+
* Exits with code 1 on startup failure.
|
|
23
|
+
*/
|
|
24
|
+
export declare function startServer(): Promise<void>;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP server for JMAP mail operations.
|
|
3
|
+
* Uses stdio transport for AI assistant communication.
|
|
4
|
+
*/
|
|
5
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
6
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
7
|
+
import { loadConfig } from '../config/schema.js';
|
|
8
|
+
import { createLogger } from '../config/logger.js';
|
|
9
|
+
import { JMAPClient } from '../jmap/client.js';
|
|
10
|
+
import { formatStartupError } from '../errors.js';
|
|
11
|
+
import { registerAllTools } from './tools/index.js';
|
|
12
|
+
/** Server version (matches package.json) */
|
|
13
|
+
const SERVER_VERSION = '0.1.0';
|
|
14
|
+
/** Server name for MCP identification */
|
|
15
|
+
const SERVER_NAME = 'mcp-twake-mail';
|
|
16
|
+
/**
|
|
17
|
+
* Create MCP server instance with JMAP client.
|
|
18
|
+
* @param config Validated configuration
|
|
19
|
+
* @param logger Pino logger (stderr only)
|
|
20
|
+
* @returns Object with server and jmapClient
|
|
21
|
+
*/
|
|
22
|
+
export function createMCPServer(config, logger) {
|
|
23
|
+
const server = new McpServer({
|
|
24
|
+
name: SERVER_NAME,
|
|
25
|
+
version: SERVER_VERSION,
|
|
26
|
+
});
|
|
27
|
+
const jmapClient = new JMAPClient(config, logger);
|
|
28
|
+
return { server, jmapClient };
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Start the MCP server with JMAP validation.
|
|
32
|
+
* Validates JMAP connection before accepting MCP requests.
|
|
33
|
+
* Exits with code 1 on startup failure.
|
|
34
|
+
*/
|
|
35
|
+
export async function startServer() {
|
|
36
|
+
let sessionUrl;
|
|
37
|
+
let logger;
|
|
38
|
+
try {
|
|
39
|
+
// Step 1: Load and validate configuration
|
|
40
|
+
const config = loadConfig();
|
|
41
|
+
sessionUrl = config.JMAP_SESSION_URL;
|
|
42
|
+
// Step 2: Initialize logger (stderr only - stdout reserved for MCP)
|
|
43
|
+
logger = createLogger(config.LOG_LEVEL);
|
|
44
|
+
logger.info({ version: SERVER_VERSION }, 'Starting mcp-twake-mail server');
|
|
45
|
+
// Step 3: Create MCP server and JMAP client
|
|
46
|
+
const { server, jmapClient } = createMCPServer(config, logger);
|
|
47
|
+
// Step 4: Validate JMAP connection BEFORE accepting MCP requests
|
|
48
|
+
const session = await jmapClient.fetchSession();
|
|
49
|
+
logger.info({ accountId: session.accountId }, 'JMAP connection validated');
|
|
50
|
+
// Step 5: Register all MCP tools
|
|
51
|
+
registerAllTools(server, jmapClient, logger);
|
|
52
|
+
// Step 6: Connect MCP server via stdio transport
|
|
53
|
+
const transport = new StdioServerTransport();
|
|
54
|
+
await server.connect(transport);
|
|
55
|
+
logger.info('MCP server running on stdio');
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
// Format error with AI-friendly message
|
|
59
|
+
const errorMessage = formatStartupError(error instanceof Error ? error : new Error(String(error)), sessionUrl);
|
|
60
|
+
// Log to stderr (NEVER stdout - reserved for MCP JSON-RPC)
|
|
61
|
+
if (logger) {
|
|
62
|
+
logger.fatal({ error }, 'Startup failed');
|
|
63
|
+
}
|
|
64
|
+
process.stderr.write(`\n${errorMessage}\n`);
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/mcp/server.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AACjF,OAAO,EAAE,UAAU,EAAe,MAAM,qBAAqB,CAAC;AAC9D,OAAO,EAAE,YAAY,EAAe,MAAM,qBAAqB,CAAC;AAChE,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAC/C,OAAO,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAClD,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAEpD,4CAA4C;AAC5C,MAAM,cAAc,GAAG,OAAO,CAAC;AAE/B,yCAAyC;AACzC,MAAM,WAAW,GAAG,gBAAgB,CAAC;AAErC;;;;;GAKG;AACH,MAAM,UAAU,eAAe,CAC7B,MAAc,EACd,MAAc;IAEd,MAAM,MAAM,GAAG,IAAI,SAAS,CAAC;QAC3B,IAAI,EAAE,WAAW;QACjB,OAAO,EAAE,cAAc;KACxB,CAAC,CAAC;IAEH,MAAM,UAAU,GAAG,IAAI,UAAU,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAElD,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC;AAChC,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW;IAC/B,IAAI,UAA8B,CAAC;IACnC,IAAI,MAA0B,CAAC;IAE/B,IAAI,CAAC;QACH,0CAA0C;QAC1C,MAAM,MAAM,GAAG,UAAU,EAAE,CAAC;QAC5B,UAAU,GAAG,MAAM,CAAC,gBAAgB,CAAC;QAErC,oEAAoE;QACpE,MAAM,GAAG,YAAY,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;QACxC,MAAM,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,cAAc,EAAE,EAAE,gCAAgC,CAAC,CAAC;QAE3E,4CAA4C;QAC5C,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,GAAG,eAAe,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QAE/D,iEAAiE;QACjE,MAAM,OAAO,GAAG,MAAM,UAAU,CAAC,YAAY,EAAE,CAAC;QAChD,MAAM,CAAC,IAAI,CAAC,EAAE,SAAS,EAAE,OAAO,CAAC,SAAS,EAAE,EAAE,2BAA2B,CAAC,CAAC;QAE3E,iCAAiC;QACjC,gBAAgB,CAAC,MAAM,EAAE,UAAU,EAAE,MAAM,CAAC,CAAC;QAE7C,iDAAiD;QACjD,MAAM,SAAS,GAAG,IAAI,oBAAoB,EAAE,CAAC;QAC7C,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;QAEhC,MAAM,CAAC,IAAI,CAAC,6BAA6B,CAAC,CAAC;IAC7C,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,wCAAwC;QACxC,MAAM,YAAY,GAAG,kBAAkB,CACrC,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,EACzD,UAAU,CACX,CAAC;QAEF,2DAA2D;QAC3D,IAAI,MAAM,EAAE,CAAC;YACX,MAAM,CAAC,KAAK,CAAC,EAAE,KAAK,EAAE,EAAE,gBAAgB,CAAC,CAAC;QAC5C,CAAC;QACD,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,YAAY,IAAI,CAAC,CAAC;QAC5C,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import type { JMAPClient } from '../../jmap/client.js';
|
|
3
|
+
import type { Logger } from '../../config/logger.js';
|
|
4
|
+
/**
|
|
5
|
+
* Metadata for a single attachment.
|
|
6
|
+
*/
|
|
7
|
+
export interface AttachmentMetadata {
|
|
8
|
+
blobId: string;
|
|
9
|
+
name: string | null;
|
|
10
|
+
type: string;
|
|
11
|
+
size: number;
|
|
12
|
+
isInline: boolean;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Determine if an attachment is inline based on RFC 8621 algorithm.
|
|
16
|
+
* isInline = has cid AND disposition !== 'attachment'
|
|
17
|
+
* @param part The body part to check
|
|
18
|
+
* @returns true if the attachment is inline
|
|
19
|
+
*/
|
|
20
|
+
export declare function isInlineAttachment(part: {
|
|
21
|
+
cid?: string | null;
|
|
22
|
+
disposition?: string | null;
|
|
23
|
+
}): boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Register attachment MCP tools with the server.
|
|
26
|
+
* @param server MCP server instance
|
|
27
|
+
* @param jmapClient JMAP client for API calls
|
|
28
|
+
* @param logger Pino logger
|
|
29
|
+
*/
|
|
30
|
+
export declare function registerAttachmentTools(server: McpServer, jmapClient: JMAPClient, logger: Logger): void;
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Attachment MCP tools - get_attachments, download_attachment.
|
|
3
|
+
* Enables AI assistants to list and download email attachments.
|
|
4
|
+
*/
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
import { writeFile, mkdir } from 'fs/promises';
|
|
7
|
+
import { join } from 'path';
|
|
8
|
+
import { homedir } from 'os';
|
|
9
|
+
/** Maximum size for inline base64 response (750KB to stay under 1MB limit with overhead) */
|
|
10
|
+
const MAX_INLINE_SIZE = 750 * 1024;
|
|
11
|
+
/**
|
|
12
|
+
* Common annotations for read-only attachment tools.
|
|
13
|
+
*/
|
|
14
|
+
const ATTACHMENT_READ_ANNOTATIONS = {
|
|
15
|
+
readOnlyHint: true,
|
|
16
|
+
destructiveHint: false,
|
|
17
|
+
idempotentHint: true,
|
|
18
|
+
openWorldHint: true,
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Determine if an attachment is inline based on RFC 8621 algorithm.
|
|
22
|
+
* isInline = has cid AND disposition !== 'attachment'
|
|
23
|
+
* @param part The body part to check
|
|
24
|
+
* @returns true if the attachment is inline
|
|
25
|
+
*/
|
|
26
|
+
export function isInlineAttachment(part) {
|
|
27
|
+
return !!part.cid && part.disposition !== 'attachment';
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Transform a JMAP body part to AttachmentMetadata.
|
|
31
|
+
* @param part JMAP body part from attachments array
|
|
32
|
+
* @returns AttachmentMetadata with isInline flag
|
|
33
|
+
*/
|
|
34
|
+
function transformAttachment(part) {
|
|
35
|
+
return {
|
|
36
|
+
blobId: part.blobId,
|
|
37
|
+
name: part.name ?? null,
|
|
38
|
+
type: part.type,
|
|
39
|
+
size: part.size,
|
|
40
|
+
isInline: isInlineAttachment(part),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Filter attachments based on optional criteria.
|
|
45
|
+
* @param attachments List of attachment metadata
|
|
46
|
+
* @param excludeInline If true, remove inline attachments
|
|
47
|
+
* @param mimeTypeFilter If provided, only include attachments matching this MIME type prefix
|
|
48
|
+
* @returns Filtered list of attachments
|
|
49
|
+
*/
|
|
50
|
+
function filterAttachments(attachments, excludeInline, mimeTypeFilter) {
|
|
51
|
+
return attachments.filter((att) => {
|
|
52
|
+
if (excludeInline && att.isInline)
|
|
53
|
+
return false;
|
|
54
|
+
if (mimeTypeFilter && !att.type.startsWith(mimeTypeFilter))
|
|
55
|
+
return false;
|
|
56
|
+
return true;
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Register attachment MCP tools with the server.
|
|
61
|
+
* @param server MCP server instance
|
|
62
|
+
* @param jmapClient JMAP client for API calls
|
|
63
|
+
* @param logger Pino logger
|
|
64
|
+
*/
|
|
65
|
+
export function registerAttachmentTools(server, jmapClient, logger) {
|
|
66
|
+
// get_attachments - list attachment metadata for an email (ATTACH-01, ATTACH-02)
|
|
67
|
+
server.registerTool('get_attachments', {
|
|
68
|
+
title: 'Get Attachments',
|
|
69
|
+
description: 'List all attachments for an email with metadata (blobId, name, type, size, isInline). Supports filtering by excludeInline and mimeTypeFilter.',
|
|
70
|
+
inputSchema: {
|
|
71
|
+
emailId: z.string().describe('The unique identifier of the email to get attachments from'),
|
|
72
|
+
excludeInline: z
|
|
73
|
+
.boolean()
|
|
74
|
+
.optional()
|
|
75
|
+
.default(false)
|
|
76
|
+
.describe('Exclude inline attachments (images embedded in HTML body)'),
|
|
77
|
+
mimeTypeFilter: z
|
|
78
|
+
.string()
|
|
79
|
+
.optional()
|
|
80
|
+
.describe('Filter by MIME type prefix (e.g., "image/", "application/pdf")'),
|
|
81
|
+
},
|
|
82
|
+
annotations: ATTACHMENT_READ_ANNOTATIONS,
|
|
83
|
+
}, async ({ emailId, excludeInline, mimeTypeFilter }) => {
|
|
84
|
+
logger.debug({ emailId, excludeInline, mimeTypeFilter }, 'get_attachments called');
|
|
85
|
+
try {
|
|
86
|
+
const session = jmapClient.getSession();
|
|
87
|
+
const response = await jmapClient.request([
|
|
88
|
+
[
|
|
89
|
+
'Email/get',
|
|
90
|
+
{
|
|
91
|
+
accountId: session.accountId,
|
|
92
|
+
ids: [emailId],
|
|
93
|
+
properties: ['attachments'],
|
|
94
|
+
bodyProperties: ['blobId', 'name', 'type', 'size', 'disposition', 'cid'],
|
|
95
|
+
},
|
|
96
|
+
'getAttachments',
|
|
97
|
+
],
|
|
98
|
+
]);
|
|
99
|
+
const result = jmapClient.parseMethodResponse(response.methodResponses[0]);
|
|
100
|
+
if (!result.success) {
|
|
101
|
+
logger.error({ error: result.error, emailId }, 'JMAP error in get_attachments');
|
|
102
|
+
return {
|
|
103
|
+
isError: true,
|
|
104
|
+
content: [
|
|
105
|
+
{
|
|
106
|
+
type: 'text',
|
|
107
|
+
text: `Failed to retrieve attachments: ${result.error?.description || result.error?.type || 'Unknown error'}`,
|
|
108
|
+
},
|
|
109
|
+
],
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
const emails = result.data?.list;
|
|
113
|
+
if (!emails || emails.length === 0) {
|
|
114
|
+
return {
|
|
115
|
+
isError: true,
|
|
116
|
+
content: [
|
|
117
|
+
{
|
|
118
|
+
type: 'text',
|
|
119
|
+
text: `Email not found: ${emailId}`,
|
|
120
|
+
},
|
|
121
|
+
],
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
// Get attachments array (may be empty or undefined)
|
|
125
|
+
const rawAttachments = emails[0].attachments || [];
|
|
126
|
+
// Transform all attachments to metadata
|
|
127
|
+
const allAttachments = rawAttachments.map(transformAttachment);
|
|
128
|
+
const totalCount = allAttachments.length;
|
|
129
|
+
// Apply filters
|
|
130
|
+
const filteredAttachments = filterAttachments(allAttachments, excludeInline, mimeTypeFilter);
|
|
131
|
+
const filteredCount = filteredAttachments.length;
|
|
132
|
+
logger.debug({ emailId, total: totalCount, filtered: filteredCount }, 'get_attachments success');
|
|
133
|
+
return {
|
|
134
|
+
content: [
|
|
135
|
+
{
|
|
136
|
+
type: 'text',
|
|
137
|
+
text: JSON.stringify({
|
|
138
|
+
emailId,
|
|
139
|
+
attachments: filteredAttachments,
|
|
140
|
+
total: totalCount,
|
|
141
|
+
filtered: filteredCount,
|
|
142
|
+
}, null, 2),
|
|
143
|
+
},
|
|
144
|
+
],
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
logger.error({ error, emailId }, 'Exception in get_attachments');
|
|
149
|
+
return {
|
|
150
|
+
isError: true,
|
|
151
|
+
content: [
|
|
152
|
+
{
|
|
153
|
+
type: 'text',
|
|
154
|
+
text: `Error retrieving attachments: ${error instanceof Error ? error.message : String(error)}`,
|
|
155
|
+
},
|
|
156
|
+
],
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
// download_attachment - download attachment content (ATTACH-03)
|
|
161
|
+
server.registerTool('download_attachment', {
|
|
162
|
+
title: 'Download Attachment',
|
|
163
|
+
description: 'Download an attachment by its blobId. Small files (< 750KB) are returned as base64. Large files are automatically saved to ~/Downloads and the path is returned.',
|
|
164
|
+
inputSchema: {
|
|
165
|
+
blobId: z.string().describe('The blob ID of the attachment to download (from get_attachments)'),
|
|
166
|
+
name: z.string().optional().describe('Filename for the attachment'),
|
|
167
|
+
type: z.string().optional().describe('MIME type of the attachment'),
|
|
168
|
+
saveToPath: z.string().optional().describe('Force save to this specific path (overrides automatic behavior)'),
|
|
169
|
+
},
|
|
170
|
+
annotations: ATTACHMENT_READ_ANNOTATIONS,
|
|
171
|
+
}, async ({ blobId, name, type, saveToPath }) => {
|
|
172
|
+
logger.debug({ blobId, name, type, saveToPath }, 'download_attachment called');
|
|
173
|
+
try {
|
|
174
|
+
// Download the blob
|
|
175
|
+
const data = await jmapClient.downloadBlob(blobId, name, type);
|
|
176
|
+
const buffer = Buffer.from(data);
|
|
177
|
+
const filename = name || 'attachment';
|
|
178
|
+
logger.debug({ blobId, size: data.byteLength, maxInline: MAX_INLINE_SIZE }, 'download_attachment downloaded');
|
|
179
|
+
// Decide whether to return inline or save to disk
|
|
180
|
+
const shouldSaveToDisk = saveToPath || data.byteLength > MAX_INLINE_SIZE;
|
|
181
|
+
if (shouldSaveToDisk) {
|
|
182
|
+
// Save to disk
|
|
183
|
+
let outputPath;
|
|
184
|
+
if (saveToPath) {
|
|
185
|
+
outputPath = saveToPath;
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
// Default to ~/Downloads
|
|
189
|
+
const downloadDir = join(homedir(), 'Downloads');
|
|
190
|
+
await mkdir(downloadDir, { recursive: true });
|
|
191
|
+
outputPath = join(downloadDir, filename);
|
|
192
|
+
}
|
|
193
|
+
await writeFile(outputPath, buffer);
|
|
194
|
+
logger.debug({ blobId, size: data.byteLength, path: outputPath }, 'download_attachment saved to disk');
|
|
195
|
+
return {
|
|
196
|
+
content: [
|
|
197
|
+
{
|
|
198
|
+
type: 'text',
|
|
199
|
+
text: JSON.stringify({
|
|
200
|
+
blobId,
|
|
201
|
+
name: filename,
|
|
202
|
+
mimeType: type || 'application/octet-stream',
|
|
203
|
+
size: data.byteLength,
|
|
204
|
+
savedTo: outputPath,
|
|
205
|
+
message: `File saved to ${outputPath} (too large for inline response)`,
|
|
206
|
+
}, null, 2),
|
|
207
|
+
},
|
|
208
|
+
],
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
// Return inline as base64
|
|
213
|
+
const base64 = buffer.toString('base64');
|
|
214
|
+
logger.debug({ blobId, size: data.byteLength }, 'download_attachment returning inline');
|
|
215
|
+
return {
|
|
216
|
+
content: [
|
|
217
|
+
{
|
|
218
|
+
type: 'text',
|
|
219
|
+
text: JSON.stringify({
|
|
220
|
+
blobId,
|
|
221
|
+
name: filename,
|
|
222
|
+
mimeType: type || 'application/octet-stream',
|
|
223
|
+
size: data.byteLength,
|
|
224
|
+
data: base64,
|
|
225
|
+
}, null, 2),
|
|
226
|
+
},
|
|
227
|
+
],
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
catch (error) {
|
|
232
|
+
logger.error({ error, blobId }, 'Exception in download_attachment');
|
|
233
|
+
return {
|
|
234
|
+
isError: true,
|
|
235
|
+
content: [
|
|
236
|
+
{
|
|
237
|
+
type: 'text',
|
|
238
|
+
text: `Error downloading attachment: ${error instanceof Error ? error.message : String(error)}`,
|
|
239
|
+
},
|
|
240
|
+
],
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
logger.debug('Attachment tools registered: get_attachments, download_attachment');
|
|
245
|
+
}
|
|
246
|
+
//# sourceMappingURL=attachment.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"attachment.js","sourceRoot":"","sources":["../../../src/mcp/tools/attachment.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AAC/C,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAC5B,OAAO,EAAE,OAAO,EAAE,MAAM,IAAI,CAAC;AAM7B,4FAA4F;AAC5F,MAAM,eAAe,GAAG,GAAG,GAAG,IAAI,CAAC;AAyBnC;;GAEG;AACH,MAAM,2BAA2B,GAAoB;IACnD,YAAY,EAAE,IAAI;IAClB,eAAe,EAAE,KAAK;IACtB,cAAc,EAAE,IAAI;IACpB,aAAa,EAAE,IAAI;CACpB,CAAC;AAEF;;;;;GAKG;AACH,MAAM,UAAU,kBAAkB,CAAC,IAA0D;IAC3F,OAAO,CAAC,CAAC,IAAI,CAAC,GAAG,IAAI,IAAI,CAAC,WAAW,KAAK,YAAY,CAAC;AACzD,CAAC;AAED;;;;GAIG;AACH,SAAS,mBAAmB,CAAC,IAAkB;IAC7C,OAAO;QACL,MAAM,EAAE,IAAI,CAAC,MAAM;QACnB,IAAI,EAAE,IAAI,CAAC,IAAI,IAAI,IAAI;QACvB,IAAI,EAAE,IAAI,CAAC,IAAI;QACf,IAAI,EAAE,IAAI,CAAC,IAAI;QACf,QAAQ,EAAE,kBAAkB,CAAC,IAAI,CAAC;KACnC,CAAC;AACJ,CAAC;AAED;;;;;;GAMG;AACH,SAAS,iBAAiB,CACxB,WAAiC,EACjC,aAAuB,EACvB,cAAuB;IAEvB,OAAO,WAAW,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE;QAChC,IAAI,aAAa,IAAI,GAAG,CAAC,QAAQ;YAAE,OAAO,KAAK,CAAC;QAChD,IAAI,cAAc,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,UAAU,CAAC,cAAc,CAAC;YAAE,OAAO,KAAK,CAAC;QACzE,OAAO,IAAI,CAAC;IACd,CAAC,CAAC,CAAC;AACL,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,uBAAuB,CACrC,MAAiB,EACjB,UAAsB,EACtB,MAAc;IAEd,iFAAiF;IACjF,MAAM,CAAC,YAAY,CACjB,iBAAiB,EACjB;QACE,KAAK,EAAE,iBAAiB;QACxB,WAAW,EACT,+IAA+I;QACjJ,WAAW,EAAE;YACX,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,4DAA4D,CAAC;YAC1F,aAAa,EAAE,CAAC;iBACb,OAAO,EAAE;iBACT,QAAQ,EAAE;iBACV,OAAO,CAAC,KAAK,CAAC;iBACd,QAAQ,CAAC,2DAA2D,CAAC;YACxE,cAAc,EAAE,CAAC;iBACd,MAAM,EAAE;iBACR,QAAQ,EAAE;iBACV,QAAQ,CAAC,gEAAgE,CAAC;SAC9E;QACD,WAAW,EAAE,2BAA2B;KACzC,EACD,KAAK,EAAE,EAAE,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,EAAE,EAAE;QACnD,MAAM,CAAC,KAAK,CAAC,EAAE,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,EAAE,wBAAwB,CAAC,CAAC;QAEnF,IAAI,CAAC;YACH,MAAM,OAAO,GAAG,UAAU,CAAC,UAAU,EAAE,CAAC;YACxC,MAAM,QAAQ,GAAG,MAAM,UAAU,CAAC,OAAO,CAAC;gBACxC;oBACE,WAAW;oBACX;wBACE,SAAS,EAAE,OAAO,CAAC,SAAS;wBAC5B,GAAG,EAAE,CAAC,OAAO,CAAC;wBACd,UAAU,EAAE,CAAC,aAAa,CAAC;wBAC3B,cAAc,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,aAAa,EAAE,KAAK,CAAC;qBACzE;oBACD,gBAAgB;iBACjB;aACF,CAAC,CAAC;YAEH,MAAM,MAAM,GAAG,UAAU,CAAC,mBAAmB,CAAC,QAAQ,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC,CAAC;YAC3E,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;gBACpB,MAAM,CAAC,KAAK,CAAC,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,EAAE,OAAO,EAAE,EAAE,+BAA+B,CAAC,CAAC;gBAChF,OAAO;oBACL,OAAO,EAAE,IAAI;oBACb,OAAO,EAAE;wBACP;4BACE,IAAI,EAAE,MAAe;4BACrB,IAAI,EAAE,mCAAmC,MAAM,CAAC,KAAK,EAAE,WAAW,IAAI,MAAM,CAAC,KAAK,EAAE,IAAI,IAAI,eAAe,EAAE;yBAC9G;qBACF;iBACF,CAAC;YACJ,CAAC;YAED,MAAM,MAAM,GAAG,MAAM,CAAC,IAAI,EAAE,IAA2D,CAAC;YACxF,IAAI,CAAC,MAAM,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;gBACnC,OAAO;oBACL,OAAO,EAAE,IAAI;oBACb,OAAO,EAAE;wBACP;4BACE,IAAI,EAAE,MAAe;4BACrB,IAAI,EAAE,oBAAoB,OAAO,EAAE;yBACpC;qBACF;iBACF,CAAC;YACJ,CAAC;YAED,oDAAoD;YACpD,MAAM,cAAc,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC,WAAW,IAAI,EAAE,CAAC;YAEnD,wCAAwC;YACxC,MAAM,cAAc,GAAG,cAAc,CAAC,GAAG,CAAC,mBAAmB,CAAC,CAAC;YAC/D,MAAM,UAAU,GAAG,cAAc,CAAC,MAAM,CAAC;YAEzC,gBAAgB;YAChB,MAAM,mBAAmB,GAAG,iBAAiB,CAAC,cAAc,EAAE,aAAa,EAAE,cAAc,CAAC,CAAC;YAC7F,MAAM,aAAa,GAAG,mBAAmB,CAAC,MAAM,CAAC;YAEjD,MAAM,CAAC,KAAK,CACV,EAAE,OAAO,EAAE,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,aAAa,EAAE,EACvD,yBAAyB,CAC1B,CAAC;YAEF,OAAO;gBACL,OAAO,EAAE;oBACP;wBACE,IAAI,EAAE,MAAe;wBACrB,IAAI,EAAE,IAAI,CAAC,SAAS,CAClB;4BACE,OAAO;4BACP,WAAW,EAAE,mBAAmB;4BAChC,KAAK,EAAE,UAAU;4BACjB,QAAQ,EAAE,aAAa;yBACxB,EACD,IAAI,EACJ,CAAC,CACF;qBACF;iBACF;aACF,CAAC;QACJ,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,CAAC,KAAK,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE,8BAA8B,CAAC,CAAC;YACjE,OAAO;gBACL,OAAO,EAAE,IAAI;gBACb,OAAO,EAAE;oBACP;wBACE,IAAI,EAAE,MAAe;wBACrB,IAAI,EAAE,iCAAiC,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE;qBAChG;iBACF;aACF,CAAC;QACJ,CAAC;IACH,CAAC,CACF,CAAC;IAEF,gEAAgE;IAChE,MAAM,CAAC,YAAY,CACjB,qBAAqB,EACrB;QACE,KAAK,EAAE,qBAAqB;QAC5B,WAAW,EACT,kKAAkK;QACpK,WAAW,EAAE;YACX,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,kEAAkE,CAAC;YAC/F,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,6BAA6B,CAAC;YACnE,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,6BAA6B,CAAC;YACnE,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,iEAAiE,CAAC;SAC9G;QACD,WAAW,EAAE,2BAA2B;KACzC,EACD,KAAK,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,UAAU,EAAE,EAAE,EAAE;QAC3C,MAAM,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,UAAU,EAAE,EAAE,4BAA4B,CAAC,CAAC;QAE/E,IAAI,CAAC;YACH,oBAAoB;YACpB,MAAM,IAAI,GAAG,MAAM,UAAU,CAAC,YAAY,CAAC,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;YAC/D,MAAM,MAAM,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACjC,MAAM,QAAQ,GAAG,IAAI,IAAI,YAAY,CAAC;YAEtC,MAAM,CAAC,KAAK,CACV,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,UAAU,EAAE,SAAS,EAAE,eAAe,EAAE,EAC7D,gCAAgC,CACjC,CAAC;YAEF,kDAAkD;YAClD,MAAM,gBAAgB,GAAG,UAAU,IAAI,IAAI,CAAC,UAAU,GAAG,eAAe,CAAC;YAEzE,IAAI,gBAAgB,EAAE,CAAC;gBACrB,eAAe;gBACf,IAAI,UAAkB,CAAC;gBACvB,IAAI,UAAU,EAAE,CAAC;oBACf,UAAU,GAAG,UAAU,CAAC;gBAC1B,CAAC;qBAAM,CAAC;oBACN,yBAAyB;oBACzB,MAAM,WAAW,GAAG,IAAI,CAAC,OAAO,EAAE,EAAE,WAAW,CAAC,CAAC;oBACjD,MAAM,KAAK,CAAC,WAAW,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;oBAC9C,UAAU,GAAG,IAAI,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAC;gBAC3C,CAAC;gBAED,MAAM,SAAS,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;gBAEpC,MAAM,CAAC,KAAK,CACV,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,UAAU,EAAE,IAAI,EAAE,UAAU,EAAE,EACnD,mCAAmC,CACpC,CAAC;gBAEF,OAAO;oBACL,OAAO,EAAE;wBACP;4BACE,IAAI,EAAE,MAAe;4BACrB,IAAI,EAAE,IAAI,CAAC,SAAS,CAClB;gCACE,MAAM;gCACN,IAAI,EAAE,QAAQ;gCACd,QAAQ,EAAE,IAAI,IAAI,0BAA0B;gCAC5C,IAAI,EAAE,IAAI,CAAC,UAAU;gCACrB,OAAO,EAAE,UAAU;gCACnB,OAAO,EAAE,iBAAiB,UAAU,kCAAkC;6BACvE,EACD,IAAI,EACJ,CAAC,CACF;yBACF;qBACF;iBACF,CAAC;YACJ,CAAC;iBAAM,CAAC;gBACN,0BAA0B;gBAC1B,MAAM,MAAM,GAAG,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;gBAEzC,MAAM,CAAC,KAAK,CACV,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,UAAU,EAAE,EACjC,sCAAsC,CACvC,CAAC;gBAEF,OAAO;oBACL,OAAO,EAAE;wBACP;4BACE,IAAI,EAAE,MAAe;4BACrB,IAAI,EAAE,IAAI,CAAC,SAAS,CAClB;gCACE,MAAM;gCACN,IAAI,EAAE,QAAQ;gCACd,QAAQ,EAAE,IAAI,IAAI,0BAA0B;gCAC5C,IAAI,EAAE,IAAI,CAAC,UAAU;gCACrB,IAAI,EAAE,MAAM;6BACb,EACD,IAAI,EACJ,CAAC,CACF;yBACF;qBACF;iBACF,CAAC;YACJ,CAAC;QACH,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,CAAC,KAAK,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,kCAAkC,CAAC,CAAC;YACpE,OAAO;gBACL,OAAO,EAAE,IAAI;gBACb,OAAO,EAAE;oBACP;wBACE,IAAI,EAAE,MAAe;wBACrB,IAAI,EAAE,iCAAiC,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE;qBAChG;iBACF;aACF,CAAC;QACJ,CAAC;IACH,CAAC,CACF,CAAC;IAEF,MAAM,CAAC,KAAK,CAAC,mEAAmE,CAAC,CAAC;AACpF,CAAC"}
|