@littlebearapps/outlook-assistant 3.13.0 → 3.14.1
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 +30 -3
- package/README.md +67 -27
- package/advanced/index.js +44 -174
- package/auth/auth-errors.js +23 -1
- package/auth/oauth-server.js +7 -1
- package/auth/token-manager.js +7 -3
- package/auth/token-storage.js +28 -30
- package/auth/tools.js +61 -82
- package/calendar/attendees.js +36 -0
- package/calendar/cancel.js +9 -25
- package/calendar/create.js +42 -48
- package/calendar/decline.js +10 -25
- package/calendar/delete.js +10 -25
- package/calendar/index.js +20 -37
- package/calendar/list.js +4 -16
- package/calendar/preview.js +461 -0
- package/calendar/update.js +55 -83
- package/categories/index.js +68 -265
- package/config.js +29 -1
- package/contacts/index.js +72 -128
- package/email/attachments.js +43 -125
- package/email/conversations.js +44 -78
- package/email/delta.js +69 -46
- package/email/draft.js +170 -103
- package/email/export.js +145 -110
- package/email/folder-utils.js +3 -2
- package/email/headers.js +11 -49
- package/email/index.js +86 -110
- package/email/list.js +4 -17
- package/email/mail-tips.js +86 -57
- package/email/mark-as-read.js +13 -49
- package/email/mime.js +39 -51
- package/email/read.js +16 -50
- package/email/search.js +47 -87
- package/email/send.js +82 -48
- package/folder/create.js +6 -25
- package/folder/delete.js +117 -38
- package/folder/index.js +19 -17
- package/folder/list.js +5 -17
- package/folder/move.js +13 -42
- package/folder/resolve.js +11 -6
- package/folder/stats.js +18 -27
- package/index.js +39 -45
- package/llms-install.md +22 -4
- package/llms.txt +20 -11
- package/outlook-auth-server.js +10 -3
- package/package.json +4 -1
- package/request-handler.js +217 -116
- package/rules/create.js +28 -71
- package/rules/index.js +52 -93
- package/rules/list.js +7 -19
- package/rules/rule-builder.js +59 -22
- package/rules/update.js +27 -61
- package/server.js +41 -0
- package/settings/index.js +162 -145
- package/tools.js +30 -0
- package/utils/field-presets.js +4 -2
- package/utils/graph-api.js +65 -22
- package/utils/logger.js +251 -0
- package/utils/mock-data.js +91 -2
- package/utils/read-only.js +59 -0
- package/utils/response-formatter.js +54 -15
- package/utils/risk-classes.js +324 -0
- package/utils/safe-write.js +372 -6
- package/utils/safety.js +247 -42
- package/utils/server-instructions.js +73 -0
- package/utils/tool-error.js +33 -0
package/request-handler.js
CHANGED
|
@@ -2,142 +2,243 @@
|
|
|
2
2
|
* MCP request dispatcher for the Outlook Assistant server.
|
|
3
3
|
*
|
|
4
4
|
* Extracted from index.js so the dispatch + error-shaping logic is
|
|
5
|
-
* unit-testable without starting the stdio transport.
|
|
5
|
+
* unit-testable without starting the stdio transport. The SDK answers
|
|
6
|
+
* `initialize` and `ping` itself and negotiates the protocol version; every
|
|
7
|
+
* other request lands here.
|
|
6
8
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* Two kinds of failure, kept apart (#276):
|
|
10
|
+
* - Protocol errors are thrown as McpError, which the SDK sends as a real
|
|
11
|
+
* JSON-RPC error: unknown method (-32601), unknown tool (-32602), or a
|
|
12
|
+
* failure inside the dispatcher itself (-32603).
|
|
13
|
+
* - Tool failures (bad arguments, a throwing handler) are returned as a
|
|
14
|
+
* visible tool-error result (`{ content: [...], isError: true }`) so the
|
|
15
|
+
* model can read them and correct itself. A content-less `{ error }` object
|
|
16
|
+
* would be coerced by the SDK into `{ content: [] }`, which clients render
|
|
17
|
+
* as EMPTY OUTPUT (#213).
|
|
12
18
|
*/
|
|
19
|
+
const { McpError, ErrorCode } = require('@modelcontextprotocol/sdk/types.js');
|
|
13
20
|
const config = require('./config');
|
|
14
21
|
const { coerceArgsAgainstSchema } = require('./utils/schema-coerce');
|
|
22
|
+
const { readOnlyRefusal } = require('./utils/read-only');
|
|
23
|
+
const { riskMeta, supportsDryRun, TOOL_RISK } = require('./utils/risk-classes');
|
|
24
|
+
const { DRY_RUN_LABEL } = require('./utils/safety');
|
|
25
|
+
const { toolError } = require('./utils/tool-error');
|
|
26
|
+
const { log, withCallContext, formatNoteValue } = require('./utils/logger');
|
|
15
27
|
|
|
16
28
|
/**
|
|
17
|
-
*
|
|
18
|
-
* @param {
|
|
19
|
-
* @returns {
|
|
29
|
+
* A visible tool-error result.
|
|
30
|
+
* @param {string} text
|
|
31
|
+
* @returns {{content: Array<{type: string, text: string}>, isError: true}}
|
|
20
32
|
*/
|
|
21
|
-
function
|
|
22
|
-
return
|
|
23
|
-
|
|
24
|
-
const { method, params, id } = request;
|
|
25
|
-
console.error(`REQUEST: ${method} [${id}]`);
|
|
33
|
+
function toolErrorResult(text) {
|
|
34
|
+
return { content: [{ type: 'text', text }], isError: true };
|
|
35
|
+
}
|
|
26
36
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
37
|
+
/**
|
|
38
|
+
* tools/list result: the public fields of every tool (never the handler).
|
|
39
|
+
* @param {Array<object>} TOOLS
|
|
40
|
+
*/
|
|
41
|
+
function listTools(TOOLS) {
|
|
42
|
+
log.debug(`tools/list: ${TOOLS.length} tools`);
|
|
43
|
+
return {
|
|
44
|
+
tools: TOOLS.map((tool) => {
|
|
45
|
+
// Client-specific flags derived from the risk map (#271), e.g.
|
|
46
|
+
// Claude's anthropic/requiresUserInteraction. Others ignore them.
|
|
47
|
+
const meta = riskMeta(tool.name);
|
|
48
|
+
return {
|
|
49
|
+
name: tool.name,
|
|
50
|
+
...(tool.title && { title: tool.title }),
|
|
51
|
+
description: tool.description,
|
|
52
|
+
inputSchema: tool.inputSchema,
|
|
53
|
+
...(tool.annotations && { annotations: tool.annotations }),
|
|
54
|
+
...(meta && { _meta: meta }),
|
|
55
|
+
};
|
|
56
|
+
}),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
44
59
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
60
|
+
/**
|
|
61
|
+
* The refusal for `dryRun: true` on a call that doesn't honour it (#274), or
|
|
62
|
+
* null. Handlers for those actions ignore the flag and really write, so the
|
|
63
|
+
* call never reaches them.
|
|
64
|
+
* @param {string} toolName
|
|
65
|
+
* @param {object} args - validated arguments
|
|
66
|
+
*/
|
|
67
|
+
function dryRunRefusal(toolName, args) {
|
|
68
|
+
if (args.dryRun !== true || supportsDryRun(toolName, args.action)) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
const action = args.action ?? TOOL_RISK[toolName]?.defaultAction;
|
|
72
|
+
const call = action ? `${toolName} action=${action}` : toolName;
|
|
73
|
+
return toolError(
|
|
74
|
+
`dryRun is not supported for ${call}; nothing was changed.`,
|
|
75
|
+
{
|
|
76
|
+
nextStep:
|
|
77
|
+
'Describe the change to the user and ask for confirmation, then call it without dryRun.',
|
|
78
|
+
}
|
|
79
|
+
);
|
|
80
|
+
}
|
|
50
81
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
82
|
+
/**
|
|
83
|
+
* Mark a supported dry run's result as a preview: `_meta.dryRun` and the
|
|
84
|
+
* DRY_RUN_LABEL first line, for handlers that don't set them themselves
|
|
85
|
+
* (send-email, draft create, manage-rules create/update).
|
|
86
|
+
* @param {object} result
|
|
87
|
+
*/
|
|
88
|
+
function labelDryRun(result) {
|
|
89
|
+
if (!result || result.isError) return result;
|
|
90
|
+
const content = Array.isArray(result.content) ? [...result.content] : [];
|
|
91
|
+
const first = content[0];
|
|
92
|
+
if (first?.type === 'text' && !first.text.startsWith(DRY_RUN_LABEL)) {
|
|
93
|
+
content[0] = { ...first, text: `${DRY_RUN_LABEL}\n\n${first.text}` };
|
|
94
|
+
}
|
|
95
|
+
return { ...result, content, _meta: { ...result._meta, dryRun: true } };
|
|
96
|
+
}
|
|
60
97
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
98
|
+
/**
|
|
99
|
+
* Run a tool's handler with validated arguments, unless read-only mode
|
|
100
|
+
* (#271) or an unsupported dryRun (#274) refuses the call first. Read-only
|
|
101
|
+
* mode is checked first, so it refuses even a supported dry run of a
|
|
102
|
+
* non-read call.
|
|
103
|
+
* @param {object} tool
|
|
104
|
+
* @param {object} args
|
|
105
|
+
*/
|
|
106
|
+
async function runTool(tool, args) {
|
|
107
|
+
if (config.READ_ONLY) {
|
|
108
|
+
const refusal = readOnlyRefusal(tool.name, args);
|
|
109
|
+
if (refusal) return refusal;
|
|
110
|
+
}
|
|
111
|
+
const refusal = dryRunRefusal(tool.name, args);
|
|
112
|
+
if (refusal) return refusal;
|
|
113
|
+
const result = await tool.handler(args);
|
|
114
|
+
return args.dryRun === true ? labelDryRun(result) : result;
|
|
115
|
+
}
|
|
64
116
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
117
|
+
/**
|
|
118
|
+
* The action to show on the call line: only a value from the tool's own
|
|
119
|
+
* `action` enum, `?` for anything else, so free text never reaches the log.
|
|
120
|
+
* @param {object|undefined} tool
|
|
121
|
+
* @param {object} args
|
|
122
|
+
* @returns {string|undefined}
|
|
123
|
+
*/
|
|
124
|
+
function loggableAction(tool, args) {
|
|
125
|
+
const action = args && args.action;
|
|
126
|
+
if (action === undefined) return undefined;
|
|
127
|
+
const allowed = tool?.inputSchema?.properties?.action?.enum;
|
|
128
|
+
return Array.isArray(allowed) && allowed.includes(action) ? action : '?';
|
|
129
|
+
}
|
|
69
130
|
|
|
70
|
-
|
|
131
|
+
/**
|
|
132
|
+
* The one default-level line per tool call (#278): tool name, action,
|
|
133
|
+
* outcome and duration, plus any notes (e.g. a Graph status) collected
|
|
134
|
+
* during the call. Never the arguments.
|
|
135
|
+
*/
|
|
136
|
+
function logToolCall({ tool, action, outcome, startedAt, notes }) {
|
|
137
|
+
const parts = [`tool=${tool}`];
|
|
138
|
+
if (action !== undefined) parts.push(`action=${action}`);
|
|
139
|
+
parts.push(`outcome=${outcome}`, `ms=${Date.now() - startedAt}`);
|
|
140
|
+
for (const [key, value] of notes) {
|
|
141
|
+
parts.push(`${key}=${formatNoteValue(value)}`);
|
|
142
|
+
}
|
|
143
|
+
log.info(parts.join(' '));
|
|
144
|
+
}
|
|
71
145
|
|
|
72
|
-
|
|
73
|
-
|
|
146
|
+
/**
|
|
147
|
+
* tools/call: validate arguments, then run the tool's handler. Logs one
|
|
148
|
+
* line per call (see logToolCall).
|
|
149
|
+
* @param {Array<object>} TOOLS
|
|
150
|
+
* @param {object} [params]
|
|
151
|
+
*/
|
|
152
|
+
function callTool(TOOLS, params) {
|
|
153
|
+
const { name, arguments: args = {} } = params || {};
|
|
154
|
+
const tool = TOOLS.find((t) => t.name === name);
|
|
155
|
+
const startedAt = Date.now();
|
|
74
156
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
],
|
|
90
|
-
isError: true,
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
return await tool.handler(coerced.args);
|
|
94
|
-
}
|
|
95
|
-
return await tool.handler(args);
|
|
96
|
-
}
|
|
157
|
+
return withCallContext(async (ctx) => {
|
|
158
|
+
const line = {
|
|
159
|
+
tool: tool ? name : '?',
|
|
160
|
+
action: loggableAction(tool, args),
|
|
161
|
+
startedAt,
|
|
162
|
+
notes: ctx.notes,
|
|
163
|
+
};
|
|
164
|
+
if (!tool || !tool.handler) {
|
|
165
|
+
logToolCall({ ...line, outcome: 'unknown-tool' });
|
|
166
|
+
throw new McpError(ErrorCode.InvalidParams, `Unknown tool: ${name}`);
|
|
167
|
+
}
|
|
168
|
+
log.debug(
|
|
169
|
+
`tools/call ${name} args: ${Object.keys(args || {}).join(', ') || '(none)'}`
|
|
170
|
+
);
|
|
97
171
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
172
|
+
const { result, error } = await runToolCall(tool, name, args);
|
|
173
|
+
if (error) {
|
|
174
|
+
// Class name only (e.g. TypeError): the message can carry user data.
|
|
175
|
+
const errorClass = /^[A-Za-z]{1,40}$/.test(error?.name)
|
|
176
|
+
? error.name
|
|
177
|
+
: 'Error';
|
|
178
|
+
ctx.notes.set('error', errorClass);
|
|
179
|
+
logToolCall({ ...line, outcome: 'thrown' });
|
|
180
|
+
} else {
|
|
181
|
+
logToolCall({ ...line, outcome: result?.isError ? 'isError' : 'ok' });
|
|
182
|
+
}
|
|
183
|
+
return result;
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Coerce and validate the arguments, then run the handler.
|
|
189
|
+
* @returns {Promise<{result: object, error?: Error}>}
|
|
190
|
+
*/
|
|
191
|
+
async function runToolCall(tool, name, args) {
|
|
192
|
+
try {
|
|
193
|
+
// Coerce + validate args against the tool's inputSchema before
|
|
194
|
+
// dispatching. Catches array-as-string, boolean-as-string, unknown
|
|
195
|
+
// params, and out-of-enum action values at the MCP boundary so
|
|
196
|
+
// handlers receive properly-typed JS values. (#160, #162)
|
|
197
|
+
if (tool.inputSchema) {
|
|
198
|
+
const coerced = coerceArgsAgainstSchema(args, tool.inputSchema);
|
|
199
|
+
if (coerced.error) {
|
|
200
|
+
return {
|
|
201
|
+
result: toolErrorResult(
|
|
202
|
+
`Invalid arguments for tool '${name}':\n${coerced.error}`
|
|
203
|
+
),
|
|
204
|
+
};
|
|
123
205
|
}
|
|
206
|
+
return { result: await runTool(tool, coerced.args) };
|
|
207
|
+
}
|
|
208
|
+
return { result: await runTool(tool, args) };
|
|
209
|
+
} catch (error) {
|
|
210
|
+
log.debug('Error in tools/call:', error);
|
|
211
|
+
return {
|
|
212
|
+
result: toolErrorResult(`Error processing tool call: ${error.message}`),
|
|
213
|
+
error,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
}
|
|
124
217
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
218
|
+
/**
|
|
219
|
+
* Build the MCP fallbackRequestHandler for a given tool set.
|
|
220
|
+
* @param {Array<{name: string, title?: string, description?: string, inputSchema?: object, annotations?: object, handler?: Function}>} TOOLS
|
|
221
|
+
* @returns {(request: object) => Promise<object>}
|
|
222
|
+
*/
|
|
223
|
+
function createRequestHandler(TOOLS) {
|
|
224
|
+
return async (request) => {
|
|
225
|
+
const { method, params, id } = request;
|
|
226
|
+
log.debug(`REQUEST: ${method} [${id}]`);
|
|
227
|
+
|
|
228
|
+
try {
|
|
229
|
+
if (method === 'tools/list') return listTools(TOOLS);
|
|
230
|
+
if (method === 'tools/call') return await callTool(TOOLS, params);
|
|
132
231
|
} catch (error) {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
}
|
|
139
|
-
|
|
232
|
+
if (error instanceof McpError) throw error;
|
|
233
|
+
log.info(`Error in fallbackRequestHandler: ${error.name || 'Error'}`);
|
|
234
|
+
log.debug('Error in fallbackRequestHandler:', error);
|
|
235
|
+
throw new McpError(
|
|
236
|
+
ErrorCode.InternalError,
|
|
237
|
+
`Error processing request: ${error.message}`
|
|
238
|
+
);
|
|
140
239
|
}
|
|
240
|
+
|
|
241
|
+
throw new McpError(ErrorCode.MethodNotFound, `Method not found: ${method}`);
|
|
141
242
|
};
|
|
142
243
|
}
|
|
143
244
|
|
package/rules/create.js
CHANGED
|
@@ -3,16 +3,17 @@
|
|
|
3
3
|
*/
|
|
4
4
|
const { callGraphAPI } = require('../utils/graph-api');
|
|
5
5
|
const { ensureAuthenticated } = require('../auth');
|
|
6
|
-
const { checkRateLimit } = require('../utils/safety');
|
|
7
|
-
const { formatRuleDryRunPreview } = require('../utils/safety');
|
|
6
|
+
const { checkRateLimit, formatRuleDryRunPreview } = require('../utils/safety');
|
|
8
7
|
const { getInboxRules } = require('./list');
|
|
9
8
|
const {
|
|
10
9
|
buildConditions,
|
|
11
10
|
buildActions,
|
|
12
11
|
buildExceptions,
|
|
12
|
+
checkRuleRecipients,
|
|
13
13
|
hasAnyCondition,
|
|
14
14
|
hasAnyAction,
|
|
15
15
|
} = require('./rule-builder');
|
|
16
|
+
const { toolError, authRequiredError } = require('../utils/tool-error');
|
|
16
17
|
|
|
17
18
|
/**
|
|
18
19
|
* Create rule handler
|
|
@@ -22,55 +23,31 @@ const {
|
|
|
22
23
|
async function handleCreateRule(args) {
|
|
23
24
|
const { name, isEnabled = true, sequence, dryRun } = args;
|
|
24
25
|
|
|
25
|
-
// Rate limit rule creation
|
|
26
|
-
const rateLimitError = checkRateLimit('manage-rules');
|
|
27
|
-
if (rateLimitError) return rateLimitError;
|
|
28
|
-
|
|
29
26
|
// Validate sequence parameter
|
|
30
27
|
if (sequence !== undefined && (isNaN(sequence) || sequence < 1)) {
|
|
31
|
-
return
|
|
32
|
-
content: [
|
|
33
|
-
{
|
|
34
|
-
type: 'text',
|
|
35
|
-
text: 'Sequence must be a positive number greater than zero.',
|
|
36
|
-
},
|
|
37
|
-
],
|
|
38
|
-
};
|
|
28
|
+
return toolError('Sequence must be a positive number greater than zero.');
|
|
39
29
|
}
|
|
40
30
|
|
|
41
31
|
if (!name) {
|
|
42
|
-
return
|
|
43
|
-
content: [
|
|
44
|
-
{
|
|
45
|
-
type: 'text',
|
|
46
|
-
text: 'Rule name is required.',
|
|
47
|
-
},
|
|
48
|
-
],
|
|
49
|
-
};
|
|
32
|
+
return toolError('Rule name is required.');
|
|
50
33
|
}
|
|
51
34
|
|
|
52
35
|
if (!hasAnyCondition(args)) {
|
|
53
|
-
return
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
type: 'text',
|
|
57
|
-
text: 'At least one condition is required. Available conditions: fromAddresses, containsSubject, bodyContains, bodyOrSubjectContains, senderContains, recipientContains, sentToAddresses, hasAttachments, importance, sensitivity, sentToMe, sentOnlyToMe, sentCcMe, isAutomaticReply.',
|
|
58
|
-
},
|
|
59
|
-
],
|
|
60
|
-
};
|
|
36
|
+
return toolError(
|
|
37
|
+
'At least one condition is required. Available conditions: fromAddresses, containsSubject, bodyContains, bodyOrSubjectContains, senderContains, recipientContains, sentToAddresses, hasAttachments, importance, sensitivity, sentToMe, sentOnlyToMe, sentCcMe, isAutomaticReply.'
|
|
38
|
+
);
|
|
61
39
|
}
|
|
62
40
|
|
|
63
41
|
if (!hasAnyAction(args)) {
|
|
64
|
-
return
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
type: 'text',
|
|
68
|
-
text: 'At least one action is required. Available actions: moveToFolder, copyToFolder, markAsRead, markImportance, forwardTo, redirectTo, assignCategories, stopProcessingRules, deleteMessage.',
|
|
69
|
-
},
|
|
70
|
-
],
|
|
71
|
-
};
|
|
42
|
+
return toolError(
|
|
43
|
+
'At least one action is required. Available actions: moveToFolder, copyToFolder, markAsRead, markImportance, forwardTo, redirectTo, assignCategories, stopProcessingRules, deleteMessage.'
|
|
44
|
+
);
|
|
72
45
|
}
|
|
73
46
|
|
|
47
|
+
// Refuse the whole rule if the allowlist blocks any forwarding (#273)
|
|
48
|
+
const recipientError = checkRuleRecipients(args, { dryRun });
|
|
49
|
+
if (recipientError) return recipientError;
|
|
50
|
+
|
|
74
51
|
try {
|
|
75
52
|
const accessToken = await ensureAuthenticated();
|
|
76
53
|
|
|
@@ -87,14 +64,9 @@ async function handleCreateRule(args) {
|
|
|
87
64
|
// Check for fatal warnings (folder not found = no valid action)
|
|
88
65
|
const folderNotFound = actWarnings.some((w) => w.includes('not found'));
|
|
89
66
|
if (folderNotFound && Object.keys(actions).length === 0) {
|
|
90
|
-
return
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
type: 'text',
|
|
94
|
-
text: actWarnings.filter((w) => w.includes('not found')).join('\n'),
|
|
95
|
-
},
|
|
96
|
-
],
|
|
97
|
-
};
|
|
67
|
+
return toolError(
|
|
68
|
+
actWarnings.filter((w) => w.includes('not found')).join('\n')
|
|
69
|
+
);
|
|
98
70
|
}
|
|
99
71
|
|
|
100
72
|
// Determine sequence
|
|
@@ -132,7 +104,7 @@ async function handleCreateRule(args) {
|
|
|
132
104
|
// Dry-run: preview without creating
|
|
133
105
|
if (dryRun) {
|
|
134
106
|
const preview = formatRuleDryRunPreview(rule);
|
|
135
|
-
let text = `
|
|
107
|
+
let text = `Rule preview (not created):\n\n${preview}`;
|
|
136
108
|
if (allWarnings.length > 0) {
|
|
137
109
|
text += `\n\nWarnings:\n${allWarnings.map((w) => `- ${w}`).join('\n')}`;
|
|
138
110
|
}
|
|
@@ -141,6 +113,10 @@ async function handleCreateRule(args) {
|
|
|
141
113
|
};
|
|
142
114
|
}
|
|
143
115
|
|
|
116
|
+
// Rate limit only real writes, so a dry run never uses up a slot (#273)
|
|
117
|
+
const rateLimitError = checkRateLimit('manage-rules');
|
|
118
|
+
if (rateLimitError) return rateLimitError;
|
|
119
|
+
|
|
144
120
|
// Create the rule
|
|
145
121
|
const response = await callGraphAPI(
|
|
146
122
|
accessToken,
|
|
@@ -167,34 +143,15 @@ async function handleCreateRule(args) {
|
|
|
167
143
|
};
|
|
168
144
|
}
|
|
169
145
|
|
|
170
|
-
return
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
type: 'text',
|
|
174
|
-
text: "Failed to create rule. The server didn't return a rule ID.",
|
|
175
|
-
},
|
|
176
|
-
],
|
|
177
|
-
};
|
|
146
|
+
return toolError(
|
|
147
|
+
"Failed to create rule. The server didn't return a rule ID."
|
|
148
|
+
);
|
|
178
149
|
} catch (error) {
|
|
179
150
|
if (error.message === 'Authentication required') {
|
|
180
|
-
return
|
|
181
|
-
content: [
|
|
182
|
-
{
|
|
183
|
-
type: 'text',
|
|
184
|
-
text: "Authentication required. Please use the 'auth' tool with action=authenticate first.",
|
|
185
|
-
},
|
|
186
|
-
],
|
|
187
|
-
};
|
|
151
|
+
return authRequiredError();
|
|
188
152
|
}
|
|
189
153
|
|
|
190
|
-
return {
|
|
191
|
-
content: [
|
|
192
|
-
{
|
|
193
|
-
type: 'text',
|
|
194
|
-
text: `Error creating rule: ${error.message}`,
|
|
195
|
-
},
|
|
196
|
-
],
|
|
197
|
-
};
|
|
154
|
+
return toolError(`Error creating rule: ${error.message}`);
|
|
198
155
|
}
|
|
199
156
|
}
|
|
200
157
|
|