@littlebearapps/outlook-assistant 3.8.1 → 3.8.2

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.
@@ -11,6 +11,10 @@ const https = require('https');
11
11
  const querystring = require('querystring');
12
12
  const config = require('../config');
13
13
 
14
+ // Fail fast if the OAuth endpoint is unreachable (e.g. blocked outbound
15
+ // egress in a sandboxed connector) instead of hanging indefinitely. (#213)
16
+ const REQUEST_TIMEOUT_MS = 15000;
17
+
14
18
  /**
15
19
  * POST helper for OAuth2 endpoints
16
20
  * @param {string} url - Full URL to POST to
@@ -41,6 +45,13 @@ function postRequest(url, postData) {
41
45
  }
42
46
  );
43
47
  req.on('error', reject);
48
+ req.setTimeout(REQUEST_TIMEOUT_MS, () => {
49
+ req.destroy(
50
+ new Error(
51
+ 'Device code request timed out — network egress to login.microsoftonline.com may be blocked.'
52
+ )
53
+ );
54
+ });
44
55
  req.write(postData);
45
56
  req.end();
46
57
  });
package/auth/tools.js CHANGED
@@ -208,10 +208,22 @@ async function handleDeviceCodeAuth() {
208
208
  }
209
209
 
210
210
  console.error('[AUTH] Starting device code flow...');
211
- const response = await initiateDeviceCodeFlow(
212
- clientId,
213
- config.AUTH_CONFIG.scopes
214
- );
211
+
212
+ // #213 — initiation can throw (blocked network egress in a sandboxed
213
+ // connector, AADSTS9002331 audience mismatch, invalid_client, non-JSON
214
+ // proxy page). Without this guard the exception propagates to the
215
+ // top-level dispatcher, which returns a content-less error and the client
216
+ // renders EMPTY OUTPUT. Mirror the try/catch that step 2
217
+ // (handleDeviceCodeComplete) already has, and surface actionable hints.
218
+ let response;
219
+ try {
220
+ response = await initiateDeviceCodeFlow(
221
+ clientId,
222
+ config.AUTH_CONFIG.scopes
223
+ );
224
+ } catch (error) {
225
+ return buildDeviceCodeErrorResponse(error);
226
+ }
215
227
 
216
228
  // Store in memory and persist to disk
217
229
  pendingDeviceCode = {
@@ -242,6 +254,57 @@ async function handleDeviceCodeAuth() {
242
254
  };
243
255
  }
244
256
 
257
+ /**
258
+ * Build a visible, actionable MCP error result for a failed device-code
259
+ * initiation. Adds remediation hints for the common remote-connector
260
+ * failure modes. (#213)
261
+ * @param {Error} error
262
+ * @returns {object} - MCP response ({ content, isError: true })
263
+ */
264
+ function buildDeviceCodeErrorResponse(error) {
265
+ const msg = (error && error.message) || String(error);
266
+ const code = error && error.code;
267
+ const hints = [];
268
+
269
+ if (/AADSTS9002331/i.test(msg) || /personal.*account/i.test(msg)) {
270
+ hints.push(
271
+ 'This app registration appears to accept personal Microsoft accounts only. Set `OUTLOOK_AUTH_AUDIENCE=consumers` (or the correct tenant GUID) in your MCP env and retry.'
272
+ );
273
+ }
274
+ if (
275
+ /invalid_client|unauthorized_client|AADSTS7000218|AADSTS700016/i.test(msg)
276
+ ) {
277
+ hints.push(
278
+ "Enable 'Allow public client flows' under Azure → App registration → Authentication → Advanced settings, and add the `nativeclient` redirect URI (Mobile and desktop applications platform)."
279
+ );
280
+ }
281
+ if (
282
+ code === 'ENOTFOUND' ||
283
+ code === 'ETIMEDOUT' ||
284
+ code === 'ECONNREFUSED' ||
285
+ code === 'ECONNRESET' ||
286
+ /timed out|ENOTFOUND|ETIMEDOUT|ECONNREFUSED|ECONNRESET|network|Failed to parse response/i.test(
287
+ msg
288
+ )
289
+ ) {
290
+ hints.push(
291
+ 'Could not reach `login.microsoftonline.com`. Outbound network access may be blocked in this environment (e.g. a sandboxed connector or corporate proxy). Check egress/firewall/proxy settings.'
292
+ );
293
+ }
294
+
295
+ const lines = ['## Device Code Authentication Failed', '', `Error: ${msg}`];
296
+ if (hints.length) {
297
+ lines.push('', 'Suggested fixes:', ...hints.map((h) => `- ${h}`));
298
+ }
299
+
300
+ console.error(`[AUTH] Device code initiation failed: ${msg}`);
301
+
302
+ return {
303
+ content: [{ type: 'text', text: lines.join('\n') }],
304
+ isError: true,
305
+ };
306
+ }
307
+
245
308
  /**
246
309
  * Device code flow step 2 — poll until the user completes authentication.
247
310
  * Checks in-memory state first, falls back to disk-persisted state.
package/index.js CHANGED
@@ -10,7 +10,7 @@ const {
10
10
  StdioServerTransport,
11
11
  } = require('@modelcontextprotocol/sdk/server/stdio.js');
12
12
  const config = require('./config');
13
- const { coerceArgsAgainstSchema } = require('./utils/schema-coerce');
13
+ const { createRequestHandler } = require('./request-handler');
14
14
 
15
15
  // Import module tools
16
16
  const { authTools, setToolCount } = require('./auth');
@@ -69,118 +69,9 @@ const server = new Server(
69
69
  }
70
70
  );
71
71
 
72
- // Handle all requests
73
- server.fallbackRequestHandler = async (request) => {
74
- try {
75
- const { method, params, id } = request;
76
- console.error(`REQUEST: ${method} [${id}]`);
77
-
78
- // Initialize handler
79
- if (method === 'initialize') {
80
- console.error(`INITIALIZE REQUEST: ID [${id}]`);
81
- return {
82
- protocolVersion: '2024-11-05',
83
- capabilities: {
84
- tools: TOOLS.reduce((acc, tool) => {
85
- acc[tool.name] = {};
86
- return acc;
87
- }, {}),
88
- },
89
- serverInfo: {
90
- name: config.SERVER_NAME,
91
- version: config.SERVER_VERSION,
92
- },
93
- };
94
- }
95
-
96
- // Tools list handler
97
- if (method === 'tools/list') {
98
- console.error(`TOOLS LIST REQUEST: ID [${id}]`);
99
- console.error(`TOOLS COUNT: ${TOOLS.length}`);
100
- console.error(`TOOLS NAMES: ${TOOLS.map((t) => t.name).join(', ')}`);
101
-
102
- return {
103
- tools: TOOLS.map((tool) => ({
104
- name: tool.name,
105
- description: tool.description,
106
- inputSchema: tool.inputSchema,
107
- ...(tool.annotations && { annotations: tool.annotations }),
108
- })),
109
- };
110
- }
111
-
112
- // Required empty responses for other capabilities
113
- if (method === 'resources/list') return { resources: [] };
114
- if (method === 'prompts/list') return { prompts: [] };
115
-
116
- // Tool call handler
117
- if (method === 'tools/call') {
118
- try {
119
- const { name, arguments: args = {} } = params || {};
120
-
121
- console.error(`TOOL CALL: ${name}`);
122
-
123
- // Find the tool handler
124
- const tool = TOOLS.find((t) => t.name === name);
125
-
126
- if (tool && tool.handler) {
127
- // Coerce + validate args against the tool's inputSchema before
128
- // dispatching. Catches array-as-string, boolean-as-string, unknown
129
- // params, and out-of-enum action values at the MCP boundary so
130
- // handlers receive properly-typed JS values. (#160, #162)
131
- if (tool.inputSchema) {
132
- const coerced = coerceArgsAgainstSchema(args, tool.inputSchema);
133
- if (coerced.error) {
134
- return {
135
- content: [
136
- {
137
- type: 'text',
138
- text: `Invalid arguments for tool '${name}':\n${coerced.error}`,
139
- },
140
- ],
141
- isError: true,
142
- };
143
- }
144
- return await tool.handler(coerced.args);
145
- }
146
- return await tool.handler(args);
147
- }
148
-
149
- // Tool not found
150
- return {
151
- error: {
152
- code: -32601,
153
- message: `Tool not found: ${name}`,
154
- },
155
- };
156
- } catch (error) {
157
- console.error(`Error in tools/call:`, error);
158
- return {
159
- error: {
160
- code: -32603,
161
- message: `Error processing tool call: ${error.message}`,
162
- },
163
- };
164
- }
165
- }
166
-
167
- // For any other method, return method not found
168
- return {
169
- error: {
170
- code: -32601,
171
- message: `Method not found: ${method}`,
172
- },
173
- };
174
- } catch (error) {
175
- console.error(`Error in fallbackRequestHandler:`, error);
176
- return {
177
- error: {
178
- code: -32603,
179
- message: `Error processing request: ${error.message}`,
180
- },
181
- };
182
- }
183
- };
72
+ // Handle all requests. Dispatch + error-shaping logic lives in
73
+ // request-handler.js so it is unit-testable without starting the transport.
74
+ server.fallbackRequestHandler = createRequestHandler(TOOLS);
184
75
 
185
76
  // Make the script executable
186
77
  process.on('SIGTERM', () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@littlebearapps/outlook-assistant",
3
- "version": "3.8.1",
3
+ "version": "3.8.2",
4
4
  "mcpName": "io.github.littlebearapps/outlook-assistant",
5
5
  "description": "Outlook Assistant — MCP server with 22 tools for email, calendar, contacts, and settings via Microsoft Graph API",
6
6
  "main": "index.js",
@@ -38,7 +38,9 @@
38
38
  "calendar",
39
39
  "contacts",
40
40
  "model-context-protocol",
41
- "anthropic"
41
+ "mcp-server",
42
+ "anthropic",
43
+ "outlook-assistant"
42
44
  ],
43
45
  "author": "Little Bear Apps <hello@littlebearapps.com>",
44
46
  "license": "MIT",