@littlebearapps/outlook-assistant 3.12.1 → 3.14.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 +27 -3
- package/README.md +108 -33
- package/advanced/index.js +44 -174
- package/auth/auth-errors.js +23 -1
- package/auth/client-config.js +142 -0
- package/auth/index.js +4 -2
- package/auth/oauth-server.js +12 -2
- package/auth/token-manager.js +7 -3
- package/auth/token-storage.js +46 -33
- package/auth/tools.js +223 -93
- 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 +335 -0
- package/calendar/update.js +42 -86
- package/categories/index.js +59 -264
- package/config.js +36 -2
- package/contacts/index.js +72 -128
- package/email/attachments.js +42 -124
- package/email/conversations.js +44 -78
- package/email/delta.js +10 -34
- package/email/draft.js +140 -96
- package/email/export.js +141 -110
- package/email/folder-utils.js +3 -2
- package/email/headers.js +11 -49
- package/email/index.js +85 -109
- 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 +14 -49
- package/email/read.js +16 -50
- package/email/search.js +46 -86
- package/email/send.js +82 -48
- package/folder/create.js +6 -25
- package/folder/delete.js +117 -38
- package/folder/index.js +17 -16
- package/folder/list.js +5 -17
- package/folder/move.js +13 -42
- package/folder/resolve.js +11 -6
- package/folder/stats.js +6 -20
- package/index.js +23 -45
- package/llms-install.md +31 -7
- package/llms.txt +19 -10
- package/outlook-auth-server.js +10 -3
- package/package.json +6 -2
- package/request-handler.js +217 -116
- package/rules/create.js +27 -70
- package/rules/index.js +30 -92
- package/rules/list.js +5 -17
- package/rules/rule-builder.js +57 -20
- package/rules/update.js +26 -60
- package/server.js +37 -0
- package/settings/index.js +142 -143
- 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 +109 -25
- package/utils/server-instructions.js +62 -0
- package/utils/tool-error.js +33 -0
package/llms-install.md
CHANGED
|
@@ -11,14 +11,23 @@ Add to your MCP client configuration:
|
|
|
11
11
|
"command": "npx",
|
|
12
12
|
"args": ["-y", "@littlebearapps/outlook-assistant"],
|
|
13
13
|
"env": {
|
|
14
|
-
"OUTLOOK_CLIENT_ID": "<user-must-provide>"
|
|
15
|
-
"OUTLOOK_CLIENT_SECRET": "<user-must-provide>"
|
|
14
|
+
"OUTLOOK_CLIENT_ID": "<user-must-provide>"
|
|
16
15
|
}
|
|
17
16
|
}
|
|
18
17
|
}
|
|
19
18
|
}
|
|
20
19
|
```
|
|
21
20
|
|
|
21
|
+
## Plugin Install (Claude Code, GitHub Copilot CLI, Cursor)
|
|
22
|
+
|
|
23
|
+
The plugin in `plugins/outlook-assistant/` runs a pinned server version and adds the `using-outlook-assistant` skill and a safety hook that asks the user before sends, deletes, rules and automatic replies.
|
|
24
|
+
|
|
25
|
+
- Claude Code: `claude plugin marketplace add littlebearapps/outlook-assistant`, then `claude plugin install outlook-assistant@littlebearapps`. The plugin asks for its settings (client ID, read-only mode, confirmation level and others) when enabled.
|
|
26
|
+
- GitHub Copilot CLI: `copilot plugin marketplace add littlebearapps/outlook-assistant`, then `copilot plugin install outlook-assistant@littlebearapps`. No plugin settings: the user gives the client ID at sign-in, and the hook's confirmation level comes from the `OUTLOOK_CONFIRM_LEVEL` environment variable (`outward`, `all-writes` or `off`).
|
|
27
|
+
- Cursor (v3.14.0 plugin or later): loads the folder as a Cursor plugin (`.cursor-plugin/plugin.json`); in Cursor CLI, pass it with `--plugin-dir`. The v3.13.0 plugin fails sign-in in Cursor with `AADSTS900023`; use the manual config instead.
|
|
28
|
+
|
|
29
|
+
Client limits (for example, Cursor's prompt doesn't show the hook's reason): `docs/how-to/getting-started/supported-clients.md`.
|
|
30
|
+
|
|
22
31
|
## Prerequisites
|
|
23
32
|
|
|
24
33
|
1. **Node.js 18.18 or newer** must be installed
|
|
@@ -36,7 +45,10 @@ Users must create an Azure app registration to get credentials:
|
|
|
36
45
|
6. Click "Register"
|
|
37
46
|
7. Copy the **Application (client) ID** → this is `OUTLOOK_CLIENT_ID`
|
|
38
47
|
|
|
39
|
-
|
|
48
|
+
If the client ID is left out of the config, the `auth` tool asks for it at sign-in (`auth action=authenticate clientId=<id>`) and saves it to `~/.outlook-assistant-config.json`.
|
|
49
|
+
|
|
50
|
+
### Create a client secret (browser flow only):
|
|
51
|
+
The default device-code sign-in doesn't need a secret; skip this unless you'll use `method=browser`, and then add `OUTLOOK_CLIENT_SECRET` to the `env` block.
|
|
40
52
|
1. Go to "Certificates & secrets" → "New client secret"
|
|
41
53
|
2. Add a description, select expiration, click "Add"
|
|
42
54
|
3. **Copy the Value immediately** (not the Secret ID) → this is `OUTLOOK_CLIENT_SECRET`
|
|
@@ -69,9 +81,18 @@ Add these to the same `env` block if needed:
|
|
|
69
81
|
|----------|---------|
|
|
70
82
|
| `OUTLOOK_AUTH_AUDIENCE` | `consumers` for Azure apps registered as personal-accounts-only (fixes `AADSTS9002331`); `organizations` or a tenant GUID for work-only apps. Default `common` |
|
|
71
83
|
| `OUTLOOK_DEFAULT_TIMEZONE` | IANA timezone for calendar times (default `Australia/Melbourne`) |
|
|
72
|
-
| `OUTLOOK_MAX_EMAILS_PER_SESSION` |
|
|
73
|
-
| `OUTLOOK_ALLOWED_RECIPIENTS` | Comma-separated allowlist of recipient domains/addresses |
|
|
84
|
+
| `OUTLOOK_MAX_EMAILS_PER_SESSION` | Default per-session cap, counted separately per tool, for `send-email` (including `draft` send), `draft` create/update/reply/reply-all/forward, `manage-rules` and `create-event` (override one tool with `OUTLOOK_MAX_<TOOL>_PER_SESSION`, e.g. `OUTLOOK_MAX_SEND_EMAIL_PER_SESSION`) |
|
|
85
|
+
| `OUTLOOK_ALLOWED_RECIPIENTS` | Comma-separated allowlist of recipient domains/addresses for sends, drafts, rule forwards and event attendees |
|
|
86
|
+
| `OUTLOOK_READ_ONLY` | `true` refuses every tool call that would change something (sending, drafts, moves, deletes, rules, settings, file writes), dry runs included; reads and sign-in still work |
|
|
74
87
|
| `OUTLOOK_SHARED_MAILBOX` | Opt-in shared-mailbox support, work/school accounts only: `read` or `true` (read and organise). Also add `Mail.Read.Shared` (and `Mail.ReadWrite.Shared` for `true`) in Azure, restart, then run `auth` with `action=authenticate` and `force=true` |
|
|
88
|
+
| `OUTLOOK_SEARCH_SCAN_LIMIT` | Messages scanned by the local search fallback on personal accounts (default 500, max 5000) |
|
|
89
|
+
| `OUTLOOK_EXPORT_DIR` | Extra folder `export` and attachment downloads may write to (besides the temp directory, `~/Downloads` and `~/Documents`) |
|
|
90
|
+
| `OUTLOOK_REQUEST_TIMEOUT_MS` | Per-attempt Graph inactivity timeout in milliseconds (default 60000); not an overall deadline |
|
|
91
|
+
| `OUTLOOK_DEBUG` | `true` for detailed stderr logs while troubleshooting (addresses and IDs redacted). Off by default: one line per tool call, no arguments |
|
|
92
|
+
|
|
93
|
+
Run `npx @littlebearapps/outlook-assistant --help` for the full list of environment variables.
|
|
94
|
+
|
|
95
|
+
`OUTLOOK_CONFIRM_LEVEL` is not a server variable: the plugin's safety hook reads it from the client's environment in GitHub Copilot, VS Code and Cursor. Don't put it in the server's `env` block.
|
|
75
96
|
|
|
76
97
|
## Configuration Files by Client
|
|
77
98
|
|
|
@@ -80,9 +101,12 @@ File: `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS)
|
|
|
80
101
|
|
|
81
102
|
### Claude Code
|
|
82
103
|
```bash
|
|
83
|
-
claude mcp add outlook -- npx @littlebearapps/outlook-assistant
|
|
104
|
+
claude mcp add outlook -e OUTLOOK_CLIENT_ID=<user-must-provide> -- npx -y @littlebearapps/outlook-assistant
|
|
84
105
|
```
|
|
85
106
|
|
|
107
|
+
### VS Code / GitHub Copilot
|
|
108
|
+
File: `.vscode/mcp.json` in your project root, or the user `mcp.json` (Command Palette → **MCP: Open User Configuration**). VS Code uses a top-level `servers` key instead of `mcpServers`, with `"type": "stdio"` on the entry.
|
|
109
|
+
|
|
86
110
|
### Cursor
|
|
87
111
|
File: `.cursor/mcp.json` in your project root
|
|
88
112
|
|
|
@@ -105,5 +129,5 @@ After authentication, test with:
|
|
|
105
129
|
| Device code "invalid_client" | Enable "Allow public client flows" in Azure → Authentication → Advanced settings |
|
|
106
130
|
| "Shared-mailbox support is turned off" | Set `OUTLOOK_SHARED_MAILBOX`, restart, and re-authenticate with `force=true` (work/school accounts only) |
|
|
107
131
|
| "EADDRINUSE :3333" | Run `npx kill-port 3333` then restart auth server |
|
|
108
|
-
|
|
|
132
|
+
| "Authentication required." | Sign in with `auth` `action=authenticate`; `action=status` shows whether a token is saved |
|
|
109
133
|
| Search returns no results (personal account) | Use `from`, `subject`, `to` filters instead of `query` |
|
package/llms.txt
CHANGED
|
@@ -33,10 +33,17 @@ Built by [Little Bear Apps](https://littlebearapps.com).
|
|
|
33
33
|
|
|
34
34
|
## Safety & Token Efficiency
|
|
35
35
|
|
|
36
|
-
- **MCP safety annotations** on all 22 tools
|
|
37
|
-
- **Send-email protections**: pre-send mail tips, dry-run preview, session rate limiting, recipient allowlist
|
|
38
|
-
- **Rule protections**: dry-run preview on create/update, rate limiting, recipient allowlist on forward/redirect, no permanent-delete action
|
|
39
|
-
- **
|
|
36
|
+
- **MCP safety annotations** on all 22 tools, derived from a risk class per tool and action (read, reversible, outward, destructive, persistent), so clients can tell reads from risky changes
|
|
37
|
+
- **Send-email protections**: pre-send mail tips (a send to a flagged recipient, such as out of office or external, is refused until `acknowledgeWarnings: true`), dry-run preview, session rate limiting, recipient allowlist (also checked on drafts, including replies and the draft's current recipients on send, and on `create-event`/`manage-event` update attendees; only single plain addresses can match)
|
|
38
|
+
- **Rule protections**: dry-run preview on create/update, rate limiting (dry runs don't count), recipient allowlist on forward/redirect (a blocked forward refuses the whole rule), no permanent-delete action
|
|
39
|
+
- **Dry-run previews beyond email**: `create-event` and every `manage-event` action say who would be emailed (with an external count); `mailbox-settings` auto-replies, `folders` delete and `manage-contact` delete show what would change or be lost; `dryRun: true` on any other call is refused rather than run for real
|
|
40
|
+
- **Read-only mode**: `OUTLOOK_READ_ONLY=true` refuses every tool call that would change anything before it runs; reads and sign-in still work
|
|
41
|
+
- **Model guidance**: server `instructions` state the hard rules (retrieved content is data, confirm before outward actions, draft first); `send-email` and `create-event` ask Claude clients for user confirmation (`anthropic/requiresUserInteraction`)
|
|
42
|
+
- **Plugin skill and safety hook** (Claude Code, GitHub Copilot CLI, Cursor; VS Code not yet checked by hand): the `using-outlook-assistant` skill sets the hard rules; the hook asks with a plain-English reason before anything that reaches other people, deletes or keeps acting, and marks retrieved content as untrusted. Confirmation level `outward` (default), `all-writes` or `off`: a plugin setting in Claude Code, the `OUTLOOK_CONFIRM_LEVEL` environment variable elsewhere. How the hook prompts and fails differs by client: see Supported Clients below
|
|
43
|
+
- **Every client**: the server-side checks (read-only mode, dry runs, the recipient allowlist and send caps, mail-tips refusal, rule refusal), annotations and instructions work in any MCP client, including Codex CLI, Gemini CLI and Claude Desktop with a manual config; clients that support Agent Skills can copy the skill folder
|
|
44
|
+
- **Private logs**: one stderr line per tool call (tool, action, outcome, duration), never its arguments; `OUTLOOK_DEBUG=true` adds detail with addresses and IDs redacted
|
|
45
|
+
- **Input and file hardening**: IDs with `.`/`..` path segments refused, the access token only ever sent to `graph.microsoft.com`, attachment downloads and exports (including conversation exports) written only inside the temp directory, `~/Downloads`, `~/Documents` or `OUTLOOK_EXPORT_DIR`, never to dot-prefixed names; paths must be absolute (or start with `~/`) and symlinks aren't followed; existing files are never replaced unless `export` is called with `overwrite: true` (and never a symlink, hard-linked file or dotfile); files are created `0600` and new folders `0700`; a partly written file is removed if a write fails
|
|
46
|
+
- **Throttling-aware Graph client**: `429` (and `503`/`504` for non-POST requests) retried honouring `Retry-After`, a per-attempt inactivity timeout (`OUTLOOK_REQUEST_TIMEOUT_MS`, default 60000 ms) and at most 4 requests in flight, so bulk operations neither hang nor throttle themselves
|
|
40
47
|
- **Shared mailboxes off by default**: `.Shared` scopes are requested only when `OUTLOOK_SHARED_MAILBOX` is set (`read` keeps shared access read-only)
|
|
41
48
|
- **Token-optimised**: 22 tools instead of 55 saves ~11,000 tokens per turn (~64% reduction), improving AI accuracy and context efficiency
|
|
42
49
|
- These safeguards reduce risk but are not foolproof — always review actions before approving
|
|
@@ -50,21 +57,20 @@ Built by [Little Bear Apps](https://littlebearapps.com).
|
|
|
50
57
|
"command": "npx",
|
|
51
58
|
"args": ["@littlebearapps/outlook-assistant"],
|
|
52
59
|
"env": {
|
|
53
|
-
"OUTLOOK_CLIENT_ID": "your-application-client-id"
|
|
54
|
-
"OUTLOOK_CLIENT_SECRET": "your-client-secret-VALUE"
|
|
60
|
+
"OUTLOOK_CLIENT_ID": "your-application-client-id"
|
|
55
61
|
}
|
|
56
62
|
}
|
|
57
63
|
}
|
|
58
64
|
}
|
|
59
65
|
```
|
|
60
66
|
|
|
61
|
-
Requires an Azure app registration with Microsoft Graph delegated permissions. See README for full setup.
|
|
67
|
+
`OUTLOOK_CLIENT_SECRET` is only needed for the browser sign-in flow; the default device-code flow uses the client ID alone. Claude Code users can instead install the plugin (server, skill and safety hook): `claude plugin marketplace add littlebearapps/outlook-assistant`, then `claude plugin install outlook-assistant@littlebearapps`. The same plugin folder also loads in GitHub Copilot CLI (VS Code not yet checked by hand) and, from v3.14.0, Cursor. Requires an Azure app registration with Microsoft Graph delegated permissions. See README for full setup.
|
|
62
68
|
|
|
63
69
|
## Tool Categories
|
|
64
70
|
|
|
65
71
|
- **Authentication (1 tool)**: `auth` — status, authenticate (device code by default), device-code-complete, about (version, granted scopes, shared-mailbox status)
|
|
66
72
|
- **Email (8 tools)**: `search-emails`, `read-email`, `send-email`, `draft`, `update-email`, `attachments`, `export`, `get-mail-tips`
|
|
67
|
-
- **Calendar (3 tools)**: `list-events` (`startAfter`, `startBefore`, `subject` filters), `create-event
|
|
73
|
+
- **Calendar (3 tools)**: `list-events` (`startAfter`, `startBefore`, `subject` filters), `create-event` (`dryRun`; retry-safe `transactionId`), `manage-event` (update, decline, cancel, delete; `dryRun` on all)
|
|
68
74
|
- **Contacts (2 tools)**: `manage-contact`, `search-people`
|
|
69
75
|
- **Folders (1 tool)**: `folders` — list, create, move, stats, delete; folders addressable by nested path (`Parent/Child`) or ID
|
|
70
76
|
- **Rules (1 tool)**: `manage-rules` — list, create, update, reorder, delete
|
|
@@ -77,12 +83,15 @@ Requires an Azure app registration with Microsoft Graph delegated permissions. S
|
|
|
77
83
|
- [README](https://raw.githubusercontent.com/littlebearapps/outlook-assistant/main/README.md): Full documentation including setup, Azure configuration, and usage
|
|
78
84
|
- [Tools Reference](https://raw.githubusercontent.com/littlebearapps/outlook-assistant/main/docs/quickrefs/tools-reference.md): All 22 tools with parameters and safety annotations
|
|
79
85
|
- [Connect Outlook to Claude](https://raw.githubusercontent.com/littlebearapps/outlook-assistant/main/docs/how-to/getting-started/connect-outlook-to-claude.md): Step-by-step setup guide for Claude Desktop / Claude Code
|
|
86
|
+
- [Supported Clients](https://raw.githubusercontent.com/littlebearapps/outlook-assistant/main/docs/how-to/getting-started/supported-clients.md): What each client gets — plugin, skill, safety hook and server-side checks — for Claude Code, GitHub Copilot, Cursor, Codex CLI, Gemini CLI and Claude Desktop
|
|
87
|
+
- [Plugin README](https://raw.githubusercontent.com/littlebearapps/outlook-assistant/main/plugins/outlook-assistant/README.md): Plugin settings, the skill and the safety hook in each client
|
|
88
|
+
- [Cross-client Matrix](https://raw.githubusercontent.com/littlebearapps/outlook-assistant/main/docs/cross-client-matrix.md): Which clients and models have been checked against the safety scenarios, and when
|
|
80
89
|
- [Verify Your Connection](https://raw.githubusercontent.com/littlebearapps/outlook-assistant/main/docs/how-to/getting-started/verify-your-connection.md): Test and troubleshoot the connection after installation
|
|
81
90
|
- [Azure Setup](https://raw.githubusercontent.com/littlebearapps/outlook-assistant/main/docs/guides/azure-setup.md): Azure app registration and API permissions walkthrough
|
|
82
91
|
- [Troubleshooting](https://raw.githubusercontent.com/littlebearapps/outlook-assistant/main/docs/troubleshooting.md): Known errors and fixes — auth, search, export, shared mailboxes
|
|
83
92
|
- [FAQ](https://raw.githubusercontent.com/littlebearapps/outlook-assistant/main/docs/faq/faq.md): Frequently asked questions — install, accounts, permissions, tokens, send safety, updates, uninstall (also at <https://littlebearapps.com/help/outlook-assistant/faq/>)
|
|
84
93
|
- [CLAUDE.md](https://raw.githubusercontent.com/littlebearapps/outlook-assistant/main/CLAUDE.md): Quick reference for development
|
|
85
94
|
- [CONTRIBUTING](https://raw.githubusercontent.com/littlebearapps/outlook-assistant/main/CONTRIBUTING.md): Contribution guidelines
|
|
86
|
-
- [CHANGELOG](https://raw.githubusercontent.com/littlebearapps/outlook-assistant/main/CHANGELOG.md): Version history (current: v3.
|
|
87
|
-
- [ROADMAP](https://raw.githubusercontent.com/littlebearapps/outlook-assistant/main/ROADMAP.md): Active milestones (v3.
|
|
95
|
+
- [CHANGELOG](https://raw.githubusercontent.com/littlebearapps/outlook-assistant/main/CHANGELOG.md): Version history (current: v3.14.0 — security and safety release: read-only mode, server instructions, more dry-run previews, mail-tips refusal, the plugin skill and a safety hook for Claude Code, GitHub Copilot and Cursor, and fixes for three advisories covering the recipient allowlist, export file writes and dry runs)
|
|
96
|
+
- [ROADMAP](https://raw.githubusercontent.com/littlebearapps/outlook-assistant/main/ROADMAP.md): Active milestones (v3.14.0 safety skill, hooks and MCP hardening; v3.15.0 structured outputs and paging; v4.0.0 MCP 2026-07-28 and server-side confirmation; the patch-release fix queue; v3.8.x carry-over; v3.16.0+ new Graph APIs)
|
|
88
97
|
- [SECURITY](https://raw.githubusercontent.com/littlebearapps/outlook-assistant/main/SECURITY.md): Security policy (report vulnerabilities privately via GitHub private vulnerability reporting; acknowledged within 7 days), token handling, and MCP safety controls
|
package/outlook-auth-server.js
CHANGED
|
@@ -35,6 +35,9 @@ require('dotenv').config();
|
|
|
35
35
|
|
|
36
36
|
// Import scopes and token path from central config to stay in sync
|
|
37
37
|
const { AUTH_CONFIG: centralAuth } = require('./config');
|
|
38
|
+
// Console output never includes the secret, the CSRF state or an auth code,
|
|
39
|
+
// and Azure error text is redacted (it can carry the user's address) (#278).
|
|
40
|
+
const { redact } = require('./utils/logger');
|
|
38
41
|
|
|
39
42
|
// Log to console
|
|
40
43
|
console.log('Starting Outlook Authentication Server');
|
|
@@ -64,7 +67,9 @@ const server = http.createServer((req, res) => {
|
|
|
64
67
|
|
|
65
68
|
if (query.error) {
|
|
66
69
|
console.error(
|
|
67
|
-
|
|
70
|
+
redact(
|
|
71
|
+
`Authentication error: ${query.error} - ${query.error_description}`
|
|
72
|
+
)
|
|
68
73
|
);
|
|
69
74
|
res.writeHead(400, SECURITY_HEADERS);
|
|
70
75
|
res.end(`
|
|
@@ -147,7 +152,7 @@ const server = http.createServer((req, res) => {
|
|
|
147
152
|
`);
|
|
148
153
|
})
|
|
149
154
|
.catch((error) => {
|
|
150
|
-
console.error(`Token exchange error: ${error.message}`);
|
|
155
|
+
console.error(redact(`Token exchange error: ${error.message}`));
|
|
151
156
|
res.writeHead(500, SECURITY_HEADERS);
|
|
152
157
|
res.end(`
|
|
153
158
|
<html>
|
|
@@ -253,7 +258,9 @@ const server = http.createServer((req, res) => {
|
|
|
253
258
|
// Use the audience from config (defaults to "common"; configurable via
|
|
254
259
|
// OUTLOOK_AUTH_AUDIENCE for personal-only / single-tenant Azure apps).
|
|
255
260
|
const authUrl = `${AUTH_CONFIG.authorizeEndpoint}?${querystring.stringify(authParams)}`;
|
|
256
|
-
console.log(
|
|
261
|
+
console.log(
|
|
262
|
+
`Redirecting to Microsoft sign-in: ${AUTH_CONFIG.authorizeEndpoint}`
|
|
263
|
+
);
|
|
257
264
|
|
|
258
265
|
// Redirect to Microsoft's login page
|
|
259
266
|
res.writeHead(302, { Location: authUrl });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@littlebearapps/outlook-assistant",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.14.0",
|
|
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",
|
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
"format": "prettier --write .",
|
|
19
19
|
"format:check": "prettier --check .",
|
|
20
20
|
"prepare": "husky || true",
|
|
21
|
-
"version": "node -
|
|
21
|
+
"version": "node scripts/sync-version.js && git add server.json plugins/outlook-assistant",
|
|
22
|
+
"version:check": "node scripts/sync-version.js --check"
|
|
22
23
|
},
|
|
23
24
|
"lint-staged": {
|
|
24
25
|
"*.js": [
|
|
@@ -59,6 +60,8 @@
|
|
|
59
60
|
"index.js",
|
|
60
61
|
"config.js",
|
|
61
62
|
"request-handler.js",
|
|
63
|
+
"server.js",
|
|
64
|
+
"tools.js",
|
|
62
65
|
"outlook-auth-server.js",
|
|
63
66
|
"auth/",
|
|
64
67
|
"calendar/",
|
|
@@ -85,6 +88,7 @@
|
|
|
85
88
|
"@commitlint/config-conventional": "^20.4.3",
|
|
86
89
|
"@eslint/js": "^10.0.1",
|
|
87
90
|
"@modelcontextprotocol/inspector": "^2.8.0",
|
|
91
|
+
"ajv": "^8.20.0",
|
|
88
92
|
"eslint": "^10.0.2",
|
|
89
93
|
"globals": "^17.4.0",
|
|
90
94
|
"husky": "^9.1.7",
|
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
|
|