@littlebearapps/outlook-assistant 3.7.2 → 3.8.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/.env.example +19 -0
- package/README.md +47 -21
- package/advanced/index.js +26 -3
- package/auth/tools.js +54 -1
- package/calendar/index.js +126 -4
- package/calendar/update.js +287 -0
- package/categories/index.js +67 -28
- package/config.js +51 -5
- package/contacts/index.js +118 -27
- package/email/attachments.js +13 -3
- package/email/conversations.js +4 -7
- package/email/delta.js +20 -6
- package/email/export.js +26 -3
- package/email/index.js +38 -4
- package/email/list.js +6 -1
- package/email/mail-tips.js +26 -2
- package/email/search.js +82 -25
- package/folder/create.js +6 -1
- package/folder/index.js +10 -1
- package/index.js +33 -0
- package/llms.txt +8 -2
- package/outlook-auth-server.js +13 -4
- package/package.json +2 -2
- package/rules/create.js +5 -1
- package/rules/index.js +19 -1
- package/rules/update.js +11 -3
- package/settings/index.js +70 -2
- package/utils/response-formatter.js +27 -0
- package/utils/schema-coerce.js +248 -0
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP boundary schema coercion + validation.
|
|
3
|
+
*
|
|
4
|
+
* Some MCP clients deliver array/boolean/number params as strings (the
|
|
5
|
+
* JSON-RPC marshalling differs across clients). Handlers were written
|
|
6
|
+
* assuming JS-typed values, so these arrive broken — arrays iterate
|
|
7
|
+
* character-by-character, `=== true` fails on `'true'`, etc.
|
|
8
|
+
*
|
|
9
|
+
* This module walks each tool's `inputSchema.properties` once at the
|
|
10
|
+
* MCP entry point and coerces incoming values into the declared types.
|
|
11
|
+
* It also enforces `additionalProperties: false`, top-level enum
|
|
12
|
+
* constraints, and `required`. Anything that fails coercion or
|
|
13
|
+
* validation gets surfaced as an MCP error response before the
|
|
14
|
+
* handler is invoked.
|
|
15
|
+
*
|
|
16
|
+
* Tracks GH #160 (param-shape mismatches) and #162 (unknown-action
|
|
17
|
+
* fallthrough — enums caught here instead of in switch defaults).
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
class CoercionError extends Error {
|
|
21
|
+
constructor(message) {
|
|
22
|
+
super(message);
|
|
23
|
+
this.name = 'CoercionError';
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Coerce a single value against a JSON schema fragment. Mutates nothing.
|
|
29
|
+
* Throws CoercionError on type mismatch that can't be resolved.
|
|
30
|
+
*/
|
|
31
|
+
function coerceValue(value, schema, path) {
|
|
32
|
+
if (value === undefined || value === null) return value;
|
|
33
|
+
if (!schema || !schema.type) return value;
|
|
34
|
+
|
|
35
|
+
const type = schema.type;
|
|
36
|
+
|
|
37
|
+
if (type === 'array') {
|
|
38
|
+
let arr = value;
|
|
39
|
+
if (typeof arr === 'string') {
|
|
40
|
+
try {
|
|
41
|
+
arr = JSON.parse(arr);
|
|
42
|
+
} catch (_e) {
|
|
43
|
+
throw new CoercionError(
|
|
44
|
+
`${path}: expected array, got non-JSON string "${truncate(value)}"`
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (!Array.isArray(arr)) {
|
|
49
|
+
throw new CoercionError(
|
|
50
|
+
`${path}: expected array, got ${describeType(arr)}`
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
if (schema.items) {
|
|
54
|
+
return arr.map((item, i) =>
|
|
55
|
+
coerceValue(item, schema.items, `${path}[${i}]`)
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
return arr;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (type === 'boolean') {
|
|
62
|
+
if (typeof value === 'boolean') return value;
|
|
63
|
+
if (value === 'true' || value === 1 || value === '1') return true;
|
|
64
|
+
if (value === 'false' || value === 0 || value === '0') return false;
|
|
65
|
+
throw new CoercionError(
|
|
66
|
+
`${path}: expected boolean, got ${describeType(value)} (${truncate(JSON.stringify(value))})`
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (type === 'integer') {
|
|
71
|
+
if (typeof value === 'number' && Number.isInteger(value)) return value;
|
|
72
|
+
if (typeof value === 'string' && value.trim() !== '') {
|
|
73
|
+
const n = Number(value);
|
|
74
|
+
if (Number.isInteger(n)) return n;
|
|
75
|
+
}
|
|
76
|
+
throw new CoercionError(
|
|
77
|
+
`${path}: expected integer, got ${describeType(value)} (${truncate(JSON.stringify(value))})`
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (type === 'number') {
|
|
82
|
+
if (typeof value === 'number') return value;
|
|
83
|
+
if (typeof value === 'string' && value.trim() !== '') {
|
|
84
|
+
const n = Number(value);
|
|
85
|
+
if (!Number.isNaN(n)) return n;
|
|
86
|
+
}
|
|
87
|
+
throw new CoercionError(
|
|
88
|
+
`${path}: expected number, got ${describeType(value)} (${truncate(JSON.stringify(value))})`
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (type === 'object') {
|
|
93
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
94
|
+
throw new CoercionError(
|
|
95
|
+
`${path}: expected object, got ${describeType(value)}`
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
if (!schema.properties) return value;
|
|
99
|
+
const result = { ...value };
|
|
100
|
+
for (const [key, propSchema] of Object.entries(schema.properties)) {
|
|
101
|
+
if (key in value) {
|
|
102
|
+
result[key] = coerceValue(value[key], propSchema, `${path}.${key}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return result;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (type === 'string') {
|
|
109
|
+
// F-24: reject arrays passed to string-typed params with a clear
|
|
110
|
+
// hint. The chokepoint pattern coerces arrays *into* arrays
|
|
111
|
+
// (F-25/F-33/F-36) but quietly let arrays slip through to string
|
|
112
|
+
// params, where they got JSON-stringified and rejected by Graph
|
|
113
|
+
// with a confusing 400. Tools whose schema declares a comma-
|
|
114
|
+
// separated string for `to`/`cc`/etc. now surface a friendly
|
|
115
|
+
// MCP-layer error before the call ever leaves the process.
|
|
116
|
+
if (Array.isArray(value)) {
|
|
117
|
+
throw new CoercionError(
|
|
118
|
+
`${path}: expected comma-separated string, got array — pass "a@example.com,b@example.com" instead of ["a@example.com","b@example.com"]`
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
// F-24 part 2 (#168): some MCP clients JSON-stringify array literals
|
|
122
|
+
// before transmission when the schema declares type:string, so the
|
|
123
|
+
// array arrives here as the literal string '["a@x.com","b@x.com"]'
|
|
124
|
+
// (brackets and quotes intact). Array.isArray returns false; the
|
|
125
|
+
// value would otherwise pass through and Graph would reject the
|
|
126
|
+
// literal-bracket address with a confusing 400.
|
|
127
|
+
if (typeof value === 'string') {
|
|
128
|
+
const trimmed = value.trim();
|
|
129
|
+
if (trimmed.startsWith('[') && trimmed.endsWith(']')) {
|
|
130
|
+
let parsed;
|
|
131
|
+
try {
|
|
132
|
+
parsed = JSON.parse(trimmed);
|
|
133
|
+
} catch (_e) {
|
|
134
|
+
parsed = undefined;
|
|
135
|
+
}
|
|
136
|
+
if (Array.isArray(parsed)) {
|
|
137
|
+
const hint = parsed.every((p) => typeof p === 'string')
|
|
138
|
+
? `"${parsed.join(',')}"`
|
|
139
|
+
: 'a comma-separated string';
|
|
140
|
+
throw new CoercionError(
|
|
141
|
+
`${path}: expected comma-separated string, got JSON-encoded array — pass ${hint} instead of ${trimmed}`
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return value;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// unknown type: pass through
|
|
150
|
+
return value;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Coerce + validate args against a tool's inputSchema. Returns
|
|
155
|
+
* { args: <coerced> } on success, or
|
|
156
|
+
* { error: '<message>' } on failure.
|
|
157
|
+
*
|
|
158
|
+
* Validates (in this order, all errors collected):
|
|
159
|
+
* 1. additionalProperties: false (rejects unknown params)
|
|
160
|
+
* 2. type coercion for each declared property (string→array/boolean/number)
|
|
161
|
+
* 3. required (rejects missing required params)
|
|
162
|
+
* 4. enum on top-level properties (rejects out-of-enum values)
|
|
163
|
+
*/
|
|
164
|
+
function coerceArgsAgainstSchema(args, inputSchema) {
|
|
165
|
+
if (!inputSchema || !inputSchema.properties) return { args: args || {} };
|
|
166
|
+
const safeArgs = args || {};
|
|
167
|
+
const errors = [];
|
|
168
|
+
|
|
169
|
+
// 1. additionalProperties: false
|
|
170
|
+
if (inputSchema.additionalProperties === false) {
|
|
171
|
+
const known = new Set(Object.keys(inputSchema.properties));
|
|
172
|
+
const unknown = Object.keys(safeArgs).filter((k) => !known.has(k));
|
|
173
|
+
if (unknown.length > 0) {
|
|
174
|
+
const validList = [...known].sort().join(', ');
|
|
175
|
+
errors.push(
|
|
176
|
+
`Unknown parameter${unknown.length > 1 ? 's' : ''}: ${unknown
|
|
177
|
+
.map((k) => `'${k}'`)
|
|
178
|
+
.join(', ')}. Valid parameters: ${validList}.`
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// 2. coerce each known property
|
|
184
|
+
const coerced = { ...safeArgs };
|
|
185
|
+
for (const [key, propSchema] of Object.entries(inputSchema.properties)) {
|
|
186
|
+
if (!(key in safeArgs)) continue;
|
|
187
|
+
try {
|
|
188
|
+
coerced[key] = coerceValue(safeArgs[key], propSchema, key);
|
|
189
|
+
} catch (e) {
|
|
190
|
+
if (e instanceof CoercionError) {
|
|
191
|
+
errors.push(e.message);
|
|
192
|
+
} else {
|
|
193
|
+
throw e;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// 3. required
|
|
199
|
+
if (Array.isArray(inputSchema.required)) {
|
|
200
|
+
for (const key of inputSchema.required) {
|
|
201
|
+
if (
|
|
202
|
+
!(key in safeArgs) ||
|
|
203
|
+
safeArgs[key] === undefined ||
|
|
204
|
+
safeArgs[key] === null ||
|
|
205
|
+
safeArgs[key] === ''
|
|
206
|
+
) {
|
|
207
|
+
errors.push(`Required parameter '${key}' is missing.`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// 4. top-level enum constraints
|
|
213
|
+
for (const [key, propSchema] of Object.entries(inputSchema.properties)) {
|
|
214
|
+
if (!(key in coerced)) continue;
|
|
215
|
+
if (!Array.isArray(propSchema.enum)) continue;
|
|
216
|
+
const v = coerced[key];
|
|
217
|
+
if (v === undefined || v === null) continue;
|
|
218
|
+
if (!propSchema.enum.includes(v)) {
|
|
219
|
+
errors.push(
|
|
220
|
+
`Parameter '${key}': value '${v}' not in allowed values [${propSchema.enum
|
|
221
|
+
.map((x) => `'${x}'`)
|
|
222
|
+
.join(', ')}].`
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (errors.length > 0) {
|
|
228
|
+
return { error: errors.join('\n') };
|
|
229
|
+
}
|
|
230
|
+
return { args: coerced };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function describeType(value) {
|
|
234
|
+
if (value === null) return 'null';
|
|
235
|
+
if (Array.isArray(value)) return 'array';
|
|
236
|
+
return typeof value;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function truncate(s, n = 60) {
|
|
240
|
+
if (typeof s !== 'string') s = String(s);
|
|
241
|
+
return s.length > n ? `${s.slice(0, n)}…` : s;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
module.exports = {
|
|
245
|
+
coerceArgsAgainstSchema,
|
|
246
|
+
coerceValue,
|
|
247
|
+
CoercionError,
|
|
248
|
+
};
|