@alfe.ai/microsoft-mcp 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/README.md +16 -0
- package/dist/server.d.ts +54 -0
- package/dist/server.js +299 -0
- package/package.json +43 -0
package/README.md
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# @alfe.ai/microsoft-mcp
|
|
2
|
+
|
|
3
|
+
Microsoft 365 MCP server — multi-account Microsoft Graph access using Alfe OAuth credentials with server-mediated token refresh
|
|
4
|
+
|
|
5
|
+
Part of [**Alfe**](https://alfe.ai) — the operating system for AI agents: build, deploy, and run agents with persistent memory, identity, integrations, and channels. See the [documentation](https://docs.alfe.ai) to get started.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @alfe.ai/microsoft-mcp
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Links
|
|
14
|
+
|
|
15
|
+
- 🌐 Website: <https://alfe.ai>
|
|
16
|
+
- 📚 Docs: <https://docs.alfe.ai>
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
|
|
3
|
+
//#region src/server.d.ts
|
|
4
|
+
|
|
5
|
+
interface MicrosoftAccountInfo {
|
|
6
|
+
connectionId: string;
|
|
7
|
+
accountIdentifier: string;
|
|
8
|
+
email: string;
|
|
9
|
+
displayName?: string;
|
|
10
|
+
connectedAt?: string;
|
|
11
|
+
microsoftTenantId?: string;
|
|
12
|
+
accessToken: string;
|
|
13
|
+
accessTokenExpiresAt: string;
|
|
14
|
+
}
|
|
15
|
+
declare function tokenIsExpired(account: MicrosoftAccountInfo): boolean;
|
|
16
|
+
interface GraphRequestSpec {
|
|
17
|
+
method: string;
|
|
18
|
+
path: string;
|
|
19
|
+
body?: unknown;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Parse the `command` string into a Graph request. Accepts a compact
|
|
23
|
+
* "METHOD /path" line (optionally followed by a JSON body), so the LLM can
|
|
24
|
+
* express any Graph operation the way it would a curl call:
|
|
25
|
+
* - "GET /me/messages?$top=5"
|
|
26
|
+
* - "GET /me"
|
|
27
|
+
* - "POST /me/sendMail {\"message\":{...}}"
|
|
28
|
+
* A bare "/path" (or "me/messages") is treated as a GET.
|
|
29
|
+
*/
|
|
30
|
+
declare function parseCommand(command: string): GraphRequestSpec;
|
|
31
|
+
/**
|
|
32
|
+
* Resolve a relative Graph path to an absolute graph.microsoft.com URL and
|
|
33
|
+
* assert the host cannot have been escaped. Defense-in-depth alongside
|
|
34
|
+
* parseCommand's absolute/protocol-relative rejection — the bearer token must
|
|
35
|
+
* never be sent anywhere but graph.microsoft.com.
|
|
36
|
+
*/
|
|
37
|
+
declare function resolveGraphUrl(path: string): string;
|
|
38
|
+
declare function createServer(): McpServer;
|
|
39
|
+
/**
|
|
40
|
+
* True when this module is the process entrypoint — i.e. `process.argv[1]`
|
|
41
|
+
* resolves to the same real file as `import.meta.url`. Used to start the stdio
|
|
42
|
+
* server only when spawned via `npx -y @alfe.ai/microsoft-mcp` / the `bin`, and
|
|
43
|
+
* to stay quiet (importable) under a test runner.
|
|
44
|
+
*
|
|
45
|
+
* CRITICAL: both sides MUST be realpath-resolved before comparing. npm/pnpm/npx
|
|
46
|
+
* install a package `bin` as a SYMLINK in a `.bin` dir, so when spawned via
|
|
47
|
+
* `npx`, `process.argv[1]` is that symlink (…/.bin/microsoft-mcp-proxy) while
|
|
48
|
+
* `import.meta.url` is the realpath target (…/dist/server.js). A plain `===` /
|
|
49
|
+
* `endsWith` check fails on that mismatch → main() never runs → the server
|
|
50
|
+
* starts and exits without connecting its transport (green-but-dead).
|
|
51
|
+
*/
|
|
52
|
+
declare function isProcessEntrypoint(argvPath: string | undefined, metaUrl: string): boolean;
|
|
53
|
+
//#endregion
|
|
54
|
+
export { MicrosoftAccountInfo, createServer, isProcessEntrypoint, parseCommand, resolveGraphUrl, tokenIsExpired };
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { realpathSync } from "node:fs";
|
|
3
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
4
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import { resolveConfig } from "@alfe.ai/config";
|
|
8
|
+
import { AgentApiClient } from "@alfe.ai/agent-api-client";
|
|
9
|
+
//#region src/server.ts
|
|
10
|
+
/**
|
|
11
|
+
* @alfe.ai/microsoft-mcp — self-contained Microsoft 365 MCP server
|
|
12
|
+
*
|
|
13
|
+
* A runtime-agnostic stdio MCP server (works on OpenClaw AND Hermes) that gives
|
|
14
|
+
* an agent multi-account Microsoft 365 access. Declared via the manifest's
|
|
15
|
+
* top-level `mcp_servers:` block (`command: npx, args: [-y, @alfe.ai/microsoft-mcp@x]`,
|
|
16
|
+
* `requires_credentials: microsoft`), like every other OAuth provider
|
|
17
|
+
* (Notion / Xero / GitHub / Atlassian / MYOB).
|
|
18
|
+
*
|
|
19
|
+
* Tools (multi-account by design — every credential-touching tool requires an
|
|
20
|
+
* explicit `email` so the LLM picks the target account per call):
|
|
21
|
+
* - microsoft_list_accounts — list connected Microsoft 365 accounts
|
|
22
|
+
* - microsoft_run_command — run a Microsoft Graph request (requires email)
|
|
23
|
+
* - microsoft_disconnect_account — disconnect an account (requires email)
|
|
24
|
+
*
|
|
25
|
+
* ── Why self-contained (not a proxy around an upstream MCP) ────────────────
|
|
26
|
+
* Notion/Xero/etc. WRAP an official upstream MCP server that accepts an
|
|
27
|
+
* injected credential. Microsoft has NO suitable upstream MCP that accepts an
|
|
28
|
+
* injected DELEGATED access token — the community M365 MCP servers do their
|
|
29
|
+
* own device/browser OAuth. So this server exposes its own tools and calls
|
|
30
|
+
* `graph.microsoft.com` REST directly.
|
|
31
|
+
*
|
|
32
|
+
* ── Why direct Graph REST, not the `mgc` CLI ───────────────────────────────
|
|
33
|
+
* The Google Workspace sibling shells out to `gws` because `gws` accepts a
|
|
34
|
+
* hand-written `authorized_user` refresh-token file and self-refreshes
|
|
35
|
+
* non-interactively. `mgc` (Microsoft Graph CLI) does NOT: its auth strategies
|
|
36
|
+
* are DeviceCode / InteractiveBrowser (both need a human at a browser —
|
|
37
|
+
* impossible on a headless agent VM) and ClientCertificate / Environment /
|
|
38
|
+
* ManagedIdentity (all APP-ONLY client-credentials, which need admin-consented
|
|
39
|
+
* *application* permissions, not the DELEGATED authorization-code refresh token
|
|
40
|
+
* our connect flow produces). mgc persists tokens in a proprietary MSAL binary
|
|
41
|
+
* cache (needs a keyring on headless Linux) with no hand-writable credentials
|
|
42
|
+
* file and no config-dir override env var — so per-account isolation via config
|
|
43
|
+
* dirs (the gws pattern) is impossible.
|
|
44
|
+
*
|
|
45
|
+
* Token refresh is server-mediated: the connect backend owns token refresh
|
|
46
|
+
* (`refreshMicrosoftAccountToken`) so the client secret never leaves the server
|
|
47
|
+
* and per-(tenant,user) refresh-token rotation stays server-side.
|
|
48
|
+
*/
|
|
49
|
+
const GRAPH_BASE = "https://graph.microsoft.com/v1.0";
|
|
50
|
+
const TOKEN_SKEW_MS = 120 * 1e3;
|
|
51
|
+
let client = null;
|
|
52
|
+
let cachedAccounts = [];
|
|
53
|
+
function log(msg) {
|
|
54
|
+
process.stderr.write(`[microsoft-mcp] ${msg}\n`);
|
|
55
|
+
}
|
|
56
|
+
function getClient() {
|
|
57
|
+
if (!client) {
|
|
58
|
+
const config = resolveConfig();
|
|
59
|
+
client = new AgentApiClient({
|
|
60
|
+
apiKey: config.apiKey,
|
|
61
|
+
apiUrl: config.apiUrl
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
return client;
|
|
65
|
+
}
|
|
66
|
+
async function refreshAccountCache() {
|
|
67
|
+
cachedAccounts = (await getClient().getMicrosoftAccounts()).accounts.map((a) => ({
|
|
68
|
+
connectionId: a.connectionId,
|
|
69
|
+
accountIdentifier: a.accountIdentifier,
|
|
70
|
+
email: a.email,
|
|
71
|
+
displayName: a.displayName ?? void 0,
|
|
72
|
+
connectedAt: a.connectedAt,
|
|
73
|
+
microsoftTenantId: a.microsoftTenantId || void 0,
|
|
74
|
+
accessToken: a.accessToken,
|
|
75
|
+
accessTokenExpiresAt: a.accessTokenExpiresAt
|
|
76
|
+
}));
|
|
77
|
+
return cachedAccounts;
|
|
78
|
+
}
|
|
79
|
+
function findAccount(email) {
|
|
80
|
+
const account = cachedAccounts.find((a) => a.email === email || a.accountIdentifier === email);
|
|
81
|
+
if (!account) throw new Error(`Microsoft account "${email}" not found. Available: ${cachedAccounts.map((a) => a.email).join(", ")}`);
|
|
82
|
+
return account;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Resolve an account by email (or accountIdentifier), refreshing the cache once
|
|
86
|
+
* if it's not already there. This server is long-lived, so an account connected
|
|
87
|
+
* AFTER the last cache fill would otherwise be invisible until a
|
|
88
|
+
* microsoft_list_accounts call — this closes that stale window. Throws
|
|
89
|
+
* findAccount's descriptive error if the account still isn't found after a refresh.
|
|
90
|
+
*/
|
|
91
|
+
async function resolveAccount(email) {
|
|
92
|
+
const hit = cachedAccounts.find((a) => a.email === email || a.accountIdentifier === email);
|
|
93
|
+
if (hit) return hit;
|
|
94
|
+
await refreshAccountCache();
|
|
95
|
+
return findAccount(email);
|
|
96
|
+
}
|
|
97
|
+
function tokenIsExpired(account) {
|
|
98
|
+
if (!account.accessToken) return true;
|
|
99
|
+
if (!account.accessTokenExpiresAt) return false;
|
|
100
|
+
const expiresAt = Date.parse(account.accessTokenExpiresAt);
|
|
101
|
+
if (Number.isNaN(expiresAt)) return false;
|
|
102
|
+
return expiresAt - Date.now() <= TOKEN_SKEW_MS;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Returns a currently-valid access token for the account, refreshing it via the
|
|
106
|
+
* connect service (which owns the client secret + refresh-token rotation) when
|
|
107
|
+
* the cached token is missing or near expiry. Setting `force` bypasses the
|
|
108
|
+
* expiry check — used to recover from a 401 on a token we believed was valid.
|
|
109
|
+
*/
|
|
110
|
+
async function ensureAccessToken(account, force = false) {
|
|
111
|
+
if (!force && !tokenIsExpired(account)) return account.accessToken;
|
|
112
|
+
const refreshed = await getClient().refreshMicrosoftAccountToken(account.accountIdentifier);
|
|
113
|
+
account.accessToken = refreshed.accessToken;
|
|
114
|
+
account.accessTokenExpiresAt = refreshed.accessTokenExpiresAt;
|
|
115
|
+
return account.accessToken;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Parse the `command` string into a Graph request. Accepts a compact
|
|
119
|
+
* "METHOD /path" line (optionally followed by a JSON body), so the LLM can
|
|
120
|
+
* express any Graph operation the way it would a curl call:
|
|
121
|
+
* - "GET /me/messages?$top=5"
|
|
122
|
+
* - "GET /me"
|
|
123
|
+
* - "POST /me/sendMail {\"message\":{...}}"
|
|
124
|
+
* A bare "/path" (or "me/messages") is treated as a GET.
|
|
125
|
+
*/
|
|
126
|
+
function parseCommand(command) {
|
|
127
|
+
const trimmed = command.trim();
|
|
128
|
+
if (!trimmed) throw new Error("Command cannot be empty");
|
|
129
|
+
const methodMatch = /^(GET|POST|PATCH|PUT|DELETE)\s+(.*)$/is.exec(trimmed);
|
|
130
|
+
let method = "GET";
|
|
131
|
+
let rest = trimmed;
|
|
132
|
+
if (methodMatch) {
|
|
133
|
+
method = methodMatch[1].toUpperCase();
|
|
134
|
+
rest = methodMatch[2].trim();
|
|
135
|
+
}
|
|
136
|
+
let path = rest;
|
|
137
|
+
let body;
|
|
138
|
+
const bodyStart = rest.search(/[{[]/);
|
|
139
|
+
if (bodyStart > 0) {
|
|
140
|
+
path = rest.slice(0, bodyStart).trim();
|
|
141
|
+
const rawBody = rest.slice(bodyStart).trim();
|
|
142
|
+
try {
|
|
143
|
+
body = JSON.parse(rawBody);
|
|
144
|
+
} catch {
|
|
145
|
+
throw new Error(`Invalid JSON body in command: ${rawBody.slice(0, 120)}`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
path = path.trim();
|
|
149
|
+
if (!path) throw new Error("Command must include a Graph path (e.g. 'GET /me/messages')");
|
|
150
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(path) || path.startsWith("//")) throw new Error("Graph path must be a relative path (e.g. '/me/messages'), not an absolute or protocol-relative URL");
|
|
151
|
+
if (!path.startsWith("/")) path = `/${path}`;
|
|
152
|
+
return {
|
|
153
|
+
method,
|
|
154
|
+
path,
|
|
155
|
+
body
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Resolve a relative Graph path to an absolute graph.microsoft.com URL and
|
|
160
|
+
* assert the host cannot have been escaped. Defense-in-depth alongside
|
|
161
|
+
* parseCommand's absolute/protocol-relative rejection — the bearer token must
|
|
162
|
+
* never be sent anywhere but graph.microsoft.com.
|
|
163
|
+
*/
|
|
164
|
+
function resolveGraphUrl(path) {
|
|
165
|
+
const url = `${GRAPH_BASE}${path.startsWith("/") ? path : `/${path}`}`;
|
|
166
|
+
let hostname;
|
|
167
|
+
try {
|
|
168
|
+
hostname = new URL(url).hostname;
|
|
169
|
+
} catch {
|
|
170
|
+
throw new Error(`Invalid Graph path: ${path.slice(0, 120)}`);
|
|
171
|
+
}
|
|
172
|
+
if (hostname.toLowerCase() !== "graph.microsoft.com") throw new Error(`Refusing to send a Microsoft token to non-Graph host: ${hostname}`);
|
|
173
|
+
return url;
|
|
174
|
+
}
|
|
175
|
+
async function graphRequest(account, spec) {
|
|
176
|
+
const url = resolveGraphUrl(spec.path);
|
|
177
|
+
const doFetch = async (token) => fetch(url, {
|
|
178
|
+
method: spec.method,
|
|
179
|
+
headers: {
|
|
180
|
+
Authorization: `Bearer ${token}`,
|
|
181
|
+
...spec.body !== void 0 ? { "Content-Type": "application/json" } : {}
|
|
182
|
+
},
|
|
183
|
+
...spec.body !== void 0 ? { body: JSON.stringify(spec.body) } : {}
|
|
184
|
+
});
|
|
185
|
+
let token = await ensureAccessToken(account);
|
|
186
|
+
let response = await doFetch(token);
|
|
187
|
+
if (response.status === 401) {
|
|
188
|
+
token = await ensureAccessToken(account, true);
|
|
189
|
+
response = await doFetch(token);
|
|
190
|
+
}
|
|
191
|
+
const text = await response.text();
|
|
192
|
+
let data = text;
|
|
193
|
+
if (text) try {
|
|
194
|
+
data = JSON.parse(text);
|
|
195
|
+
} catch {}
|
|
196
|
+
else data = null;
|
|
197
|
+
return {
|
|
198
|
+
status: response.status,
|
|
199
|
+
ok: response.ok,
|
|
200
|
+
data
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
function jsonResult(data) {
|
|
204
|
+
return { content: [{
|
|
205
|
+
type: "text",
|
|
206
|
+
text: JSON.stringify(data)
|
|
207
|
+
}] };
|
|
208
|
+
}
|
|
209
|
+
function createServer() {
|
|
210
|
+
const server = new McpServer({
|
|
211
|
+
name: "microsoft-mcp",
|
|
212
|
+
version: "0.0.1"
|
|
213
|
+
});
|
|
214
|
+
const registerTool = server.registerTool.bind(server);
|
|
215
|
+
registerTool("microsoft_list_accounts", {
|
|
216
|
+
description: "List all connected Microsoft 365 accounts. Shows email, display name, and when each account was connected. Use this to resolve which account to target (e.g., 'Kevin's mail' → kevin@contoso.com) before calling microsoft_run_command.",
|
|
217
|
+
inputSchema: {}
|
|
218
|
+
}, async () => {
|
|
219
|
+
const accounts = await refreshAccountCache();
|
|
220
|
+
return jsonResult({
|
|
221
|
+
accounts: accounts.map((a) => ({
|
|
222
|
+
email: a.email,
|
|
223
|
+
displayName: a.displayName,
|
|
224
|
+
connectedAt: a.connectedAt
|
|
225
|
+
})),
|
|
226
|
+
count: accounts.length
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
registerTool("microsoft_run_command", {
|
|
230
|
+
description: "Call the Microsoft Graph API for a specific Microsoft 365 account. The `command` is a Graph REST request line — an optional HTTP method, a Graph path (relative to https://graph.microsoft.com/v1.0), and an optional JSON body. Email is required — call microsoft_list_accounts first if you don't know which account to use. Examples: microsoft_run_command({ command: 'GET /me/messages?$top=5&$select=subject,from,receivedDateTime', email: 'kevin@contoso.com' }); microsoft_run_command({ command: 'GET /me/calendarView?startDateTime=2026-07-08T00:00:00Z&endDateTime=2026-07-09T00:00:00Z', email: 'kevin@contoso.com' }); microsoft_run_command({ command: 'POST /me/sendMail {\"message\":{\"subject\":\"Hi\",\"body\":{\"contentType\":\"Text\",\"content\":\"Hello\"},\"toRecipients\":[{\"emailAddress\":{\"address\":\"a@b.com\"}}]}}', email: 'kevin@contoso.com' }). A bare path with no method is treated as GET.",
|
|
231
|
+
inputSchema: {
|
|
232
|
+
command: z.string().describe("The Graph request: '[METHOD] /path [json-body]'. Method defaults to GET. Path is relative to https://graph.microsoft.com/v1.0 (e.g. '/me/messages')."),
|
|
233
|
+
email: z.string().describe("Email of the Microsoft 365 account to use. Required — there is no implicit default.")
|
|
234
|
+
}
|
|
235
|
+
}, async ({ command, email }) => {
|
|
236
|
+
const account = await resolveAccount(email);
|
|
237
|
+
const spec = parseCommand(command);
|
|
238
|
+
const result = await graphRequest(account, spec);
|
|
239
|
+
return jsonResult({
|
|
240
|
+
account: account.email,
|
|
241
|
+
request: `${spec.method} ${spec.path}`,
|
|
242
|
+
status: result.status,
|
|
243
|
+
ok: result.ok,
|
|
244
|
+
data: result.data
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
registerTool("microsoft_disconnect_account", {
|
|
248
|
+
description: "Disconnect a specific Microsoft 365 account from this agent. Revokes the connection and removes the account.",
|
|
249
|
+
inputSchema: { email: z.string().describe("Email of the Microsoft 365 account to disconnect") }
|
|
250
|
+
}, async ({ email }) => {
|
|
251
|
+
const account = await resolveAccount(email);
|
|
252
|
+
const result = await getClient().disconnectMicrosoftAccount(account.accountIdentifier);
|
|
253
|
+
await refreshAccountCache();
|
|
254
|
+
return jsonResult({
|
|
255
|
+
message: `${account.email} has been disconnected`,
|
|
256
|
+
remainingAccounts: result.accounts
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
return server;
|
|
260
|
+
}
|
|
261
|
+
async function main() {
|
|
262
|
+
const server = createServer();
|
|
263
|
+
try {
|
|
264
|
+
const accounts = await refreshAccountCache();
|
|
265
|
+
log(`Cached ${String(accounts.length)} Microsoft account(s): ${accounts.map((a) => a.email).join(", ")}`);
|
|
266
|
+
} catch (err) {
|
|
267
|
+
log(`Failed to pre-cache Microsoft accounts (will fetch on demand): ${err instanceof Error ? err.message : String(err)}`);
|
|
268
|
+
}
|
|
269
|
+
const transport = new StdioServerTransport();
|
|
270
|
+
await server.connect(transport);
|
|
271
|
+
log("Microsoft 365 MCP server running (multi-account, direct Graph REST)");
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* True when this module is the process entrypoint — i.e. `process.argv[1]`
|
|
275
|
+
* resolves to the same real file as `import.meta.url`. Used to start the stdio
|
|
276
|
+
* server only when spawned via `npx -y @alfe.ai/microsoft-mcp` / the `bin`, and
|
|
277
|
+
* to stay quiet (importable) under a test runner.
|
|
278
|
+
*
|
|
279
|
+
* CRITICAL: both sides MUST be realpath-resolved before comparing. npm/pnpm/npx
|
|
280
|
+
* install a package `bin` as a SYMLINK in a `.bin` dir, so when spawned via
|
|
281
|
+
* `npx`, `process.argv[1]` is that symlink (…/.bin/microsoft-mcp-proxy) while
|
|
282
|
+
* `import.meta.url` is the realpath target (…/dist/server.js). A plain `===` /
|
|
283
|
+
* `endsWith` check fails on that mismatch → main() never runs → the server
|
|
284
|
+
* starts and exits without connecting its transport (green-but-dead).
|
|
285
|
+
*/
|
|
286
|
+
function isProcessEntrypoint(argvPath, metaUrl) {
|
|
287
|
+
try {
|
|
288
|
+
if (!argvPath) return false;
|
|
289
|
+
return pathToFileURL(realpathSync(argvPath)).href === pathToFileURL(realpathSync(fileURLToPath(metaUrl))).href;
|
|
290
|
+
} catch {
|
|
291
|
+
return false;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
if (isProcessEntrypoint(process.argv[1], import.meta.url)) main().catch((err) => {
|
|
295
|
+
log(`Fatal: ${err instanceof Error ? err.message : String(err)}`);
|
|
296
|
+
process.exit(1);
|
|
297
|
+
});
|
|
298
|
+
//#endregion
|
|
299
|
+
export { createServer, isProcessEntrypoint, parseCommand, resolveGraphUrl, tokenIsExpired };
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@alfe.ai/microsoft-mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Microsoft 365 MCP server — multi-account Microsoft Graph access using Alfe OAuth credentials with server-mediated token refresh",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/server.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"microsoft-mcp-proxy": "./dist/server.js"
|
|
9
|
+
},
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/server.d.ts",
|
|
13
|
+
"import": "./dist/server.js"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist"
|
|
18
|
+
],
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"@modelcontextprotocol/sdk": ">=1.24.0",
|
|
21
|
+
"zod": "^4.0.5",
|
|
22
|
+
"@alfe.ai/config": "0.3.0",
|
|
23
|
+
"@alfe.ai/agent-api-client": "0.9.0"
|
|
24
|
+
},
|
|
25
|
+
"license": "UNLICENSED",
|
|
26
|
+
"homepage": "https://alfe.ai",
|
|
27
|
+
"author": "Alfe (https://alfe.ai)",
|
|
28
|
+
"keywords": [
|
|
29
|
+
"alfe",
|
|
30
|
+
"ai-agents",
|
|
31
|
+
"agent",
|
|
32
|
+
"llm",
|
|
33
|
+
"mcp",
|
|
34
|
+
"model-context-protocol"
|
|
35
|
+
],
|
|
36
|
+
"scripts": {
|
|
37
|
+
"build": "tsdown",
|
|
38
|
+
"dev": "tsdown --watch",
|
|
39
|
+
"typecheck": "tsc --noEmit",
|
|
40
|
+
"lint": "eslint .",
|
|
41
|
+
"test": "vitest run"
|
|
42
|
+
}
|
|
43
|
+
}
|