@frustrated/ms-graph-mcp 0.1.10 → 0.1.11
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 +3 -0
- package/docs/mcp-smoke-test.md +89 -0
- package/docs/tools/README.md +1 -1
- package/docs/tools/onedrive.md +44 -0
- package/package.json +3 -1
- package/src/auth.ts +3 -1
- package/src/index.ts +1 -1
- package/src/mcp-interface.ts +71 -1
- package/src/tools/calendar.ts +22 -7
- package/src/tools/index.ts +5 -2
- package/src/tools/mail.ts +30 -4
- package/src/tools/onedrive.ts +145 -0
- package/src/utils.ts +29 -1
- package/src/vendor/zod-v3.ts +4 -0
- package/src/vendor/zod-v4.ts +4 -0
package/README.md
CHANGED
|
@@ -20,6 +20,7 @@ A TypeScript MCP (Model Context Protocol) package for personal Microsoft Graph a
|
|
|
20
20
|
- **Secure Token Storage:** Stores the MSAL token cache at `~/.config/ms-graph-mcp/msal_cache.json` with restricted file permissions (`0600`).
|
|
21
21
|
- **User Control:** Provides CLI commands for permission management and revocation.
|
|
22
22
|
- **MCP Standard:** Adheres to the Model Context Protocol for broad agent compatibility.
|
|
23
|
+
- **OneDrive Support:** Lists, inspects, searches, and creates OneDrive files and folders.
|
|
23
24
|
|
|
24
25
|
## Usage with Manus Agents
|
|
25
26
|
|
|
@@ -54,6 +55,7 @@ The Microsoft Graph MCP CLI exposes the following tools:
|
|
|
54
55
|
|
|
55
56
|
- **`mail`**: Manage email communications (e.g., list messages).
|
|
56
57
|
- **`calendar`**: Organize calendar events (e.g., create events).
|
|
58
|
+
- **`onedrive`**: Work with OneDrive files and folders.
|
|
57
59
|
|
|
58
60
|
For detailed information on specific tools and their functionalities, refer to the [Tools Documentation](./docs/tools/README.md).
|
|
59
61
|
|
|
@@ -78,6 +80,7 @@ bunx --bun @frustrated/ms-graph-mcp revoke
|
|
|
78
80
|
- [Ideation Document](./docs/ms_graph_mcp_ideation.md) - Conceptual design and motivation.
|
|
79
81
|
- [Technical Specification](./docs/ms_graph_mcp_spec.md) - Detailed implementation guide.
|
|
80
82
|
- [Tools Documentation](./docs/tools/README.md) - Comprehensive guide to all available MCP tools.
|
|
83
|
+
- [MCP Smoke Test](./docs/mcp-smoke-test.md) - How to test the server directly over stdio with the MCP SDK.
|
|
81
84
|
|
|
82
85
|
## Security Considerations
|
|
83
86
|
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# MCP smoke testing
|
|
2
|
+
|
|
3
|
+
This guide shows how to test `ms-graph-mcp` as a raw MCP server over `stdio`, without wiring it into another AI app first.
|
|
4
|
+
|
|
5
|
+
The goal is to verify three things:
|
|
6
|
+
|
|
7
|
+
1. The process starts cleanly.
|
|
8
|
+
2. The MCP handshake succeeds.
|
|
9
|
+
3. `tools/list` and at least one `tools/call` work end to end.
|
|
10
|
+
|
|
11
|
+
## What to expect
|
|
12
|
+
|
|
13
|
+
When the server runs in MCP mode, `stdout` must remain reserved for protocol messages only. Human-readable logs should go to `stderr`.
|
|
14
|
+
|
|
15
|
+
If you see plain text on `stdout`, the MCP framing is broken.
|
|
16
|
+
|
|
17
|
+
## Prerequisites
|
|
18
|
+
|
|
19
|
+
1. Authenticate once:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
bun run src/index.ts init
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
2. Confirm the server can start:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
bun run src/index.ts run
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Manual smoke test with the MCP SDK
|
|
32
|
+
|
|
33
|
+
The easiest direct test is to use the official MCP SDK from a short Bun script.
|
|
34
|
+
|
|
35
|
+
Create a temporary file such as `scripts/mcp-smoke-test.ts` with the following contents:
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
39
|
+
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
40
|
+
|
|
41
|
+
const transport = new StdioClientTransport({
|
|
42
|
+
command: 'bun',
|
|
43
|
+
args: ['run', 'src/index.ts', 'run'],
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const client = new Client(
|
|
47
|
+
{
|
|
48
|
+
name: 'mcp-smoke-test',
|
|
49
|
+
version: '1.0.0',
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
capabilities: {},
|
|
53
|
+
},
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
await client.connect(transport);
|
|
57
|
+
|
|
58
|
+
const tools = await client.listTools();
|
|
59
|
+
console.log(JSON.stringify(tools, null, 2));
|
|
60
|
+
|
|
61
|
+
// Pick one tool that is enabled in your config.
|
|
62
|
+
// Example: mail.list_messages
|
|
63
|
+
const result = await client.callTool({
|
|
64
|
+
name: 'mail.list_messages',
|
|
65
|
+
arguments: { top: 1 },
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
console.log(JSON.stringify(result, null, 2));
|
|
69
|
+
await client.close();
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Run it with:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
bun run scripts/mcp-smoke-test.ts
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## What to verify
|
|
79
|
+
|
|
80
|
+
- `listTools()` returns the registered tools you expect.
|
|
81
|
+
- `callTool()` returns a successful result object.
|
|
82
|
+
- The server does not print protocol text to `stderr` or `stdout` outside the MCP response stream.
|
|
83
|
+
- A bad input should fail with a clear validation or Graph error instead of an opaque transport error.
|
|
84
|
+
|
|
85
|
+
## Quick debug checks
|
|
86
|
+
|
|
87
|
+
- If `listTools()` is empty, confirm tool gating in `~/.ms-graph-mcp/config.json`.
|
|
88
|
+
- If a tool call returns `401`, re-run `bun run src/index.ts init`.
|
|
89
|
+
- If a tool call returns `400`, inspect the request payload for invalid dates, missing required fields, or malformed OneDrive paths.
|
package/docs/tools/README.md
CHANGED
|
@@ -6,7 +6,7 @@ This document provides an overview of the top-level tools available in the Micro
|
|
|
6
6
|
|
|
7
7
|
* **[Mail](./mail.md)**: Interact with email messages, folders, and other mail-related functionalities.
|
|
8
8
|
* **[Calendar](./calendar.md)**: Manage calendar events, schedules, and attendees.
|
|
9
|
-
* **OneDrive
|
|
9
|
+
* **[OneDrive](./onedrive.md)**: List, inspect, search, and create files and folders in OneDrive.
|
|
10
10
|
|
|
11
11
|
## How to Discover Sub-Tools
|
|
12
12
|
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# OneDrive Tools
|
|
2
|
+
|
|
3
|
+
This document details the sub-tools available under the `onedrive` top-level tool.
|
|
4
|
+
|
|
5
|
+
## `onedrive.list_items`
|
|
6
|
+
|
|
7
|
+
Lists files and folders from OneDrive root or a specific folder.
|
|
8
|
+
|
|
9
|
+
### Parameters
|
|
10
|
+
|
|
11
|
+
* `folderPath` (string, optional): A folder path relative to OneDrive root, such as `/Projects`.
|
|
12
|
+
* `itemId` (string, optional): A drive item ID whose children should be listed.
|
|
13
|
+
* `top` (number, optional): Maximum number of items to return.
|
|
14
|
+
* `select` (string, optional): Optional OData `$select` clause.
|
|
15
|
+
|
|
16
|
+
## `onedrive.get_item`
|
|
17
|
+
|
|
18
|
+
Gets metadata for a OneDrive file or folder.
|
|
19
|
+
|
|
20
|
+
### Parameters
|
|
21
|
+
|
|
22
|
+
* `path` (string, optional): Item path relative to OneDrive root, such as `/Projects/report.docx`.
|
|
23
|
+
* `itemId` (string, optional): Drive item ID.
|
|
24
|
+
* `select` (string, optional): Optional OData `$select` clause.
|
|
25
|
+
|
|
26
|
+
## `onedrive.search_items`
|
|
27
|
+
|
|
28
|
+
Searches OneDrive for files and folders.
|
|
29
|
+
|
|
30
|
+
### Parameters
|
|
31
|
+
|
|
32
|
+
* `query` (string, required): Search query.
|
|
33
|
+
* `top` (number, optional): Maximum number of items to return.
|
|
34
|
+
* `select` (string, optional): Optional OData `$select` clause.
|
|
35
|
+
|
|
36
|
+
## `onedrive.create_folder`
|
|
37
|
+
|
|
38
|
+
Creates a folder in OneDrive root or under a specific parent folder.
|
|
39
|
+
|
|
40
|
+
### Parameters
|
|
41
|
+
|
|
42
|
+
* `name` (string, required): Folder name.
|
|
43
|
+
* `parentPath` (string, optional): Parent folder path relative to OneDrive root.
|
|
44
|
+
* `parentItemId` (string, optional): Parent drive item ID.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frustrated/ms-graph-mcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.11",
|
|
4
4
|
"description": "A JSR-based TypeScript MCP package for personal Microsoft Graph access via CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -21,6 +21,8 @@
|
|
|
21
21
|
"@microsoft/microsoft-graph-client": "npm:@microsoft/microsoft-graph-client@^3.0.0",
|
|
22
22
|
"@commander-js/extra-typings": "npm:@commander-js/extra-typings@^14.0.0",
|
|
23
23
|
"@modelcontextprotocol/sdk": "npm:@modelcontextprotocol/sdk@^1.28.0",
|
|
24
|
+
"zod/v3": "./src/vendor/zod-v3.ts",
|
|
25
|
+
"zod/v4": "./src/vendor/zod-v4.ts",
|
|
24
26
|
"zod": "npm:zod@^3.24.2"
|
|
25
27
|
},
|
|
26
28
|
"dependencies": {
|
package/src/auth.ts
CHANGED
|
@@ -18,7 +18,9 @@ const MSAL_CONFIG: Configuration = {
|
|
|
18
18
|
system: {
|
|
19
19
|
loggerOptions: {
|
|
20
20
|
loggerCallback(loglevel, message, containsPii) {
|
|
21
|
-
|
|
21
|
+
if (!containsPii) {
|
|
22
|
+
process.stderr.write(`${message}\n`);
|
|
23
|
+
}
|
|
22
24
|
},
|
|
23
25
|
piiLoggingEnabled: false,
|
|
24
26
|
logLevel: LogLevel.Verbose,
|
package/src/index.ts
CHANGED
package/src/mcp-interface.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { log } from './utils.ts';
|
|
|
7
7
|
import { Client } from '@microsoft/microsoft-graph-client';
|
|
8
8
|
import * as mail from './tools/mail.ts';
|
|
9
9
|
import * as calendar from './tools/calendar.ts';
|
|
10
|
+
import * as onedrive from './tools/onedrive.ts';
|
|
10
11
|
|
|
11
12
|
function buildGraphClient(accessToken: string): Client {
|
|
12
13
|
return Client.init({
|
|
@@ -17,7 +18,7 @@ function buildGraphClient(accessToken: string): Client {
|
|
|
17
18
|
export async function startMcpServer(): Promise<void> {
|
|
18
19
|
const server = new McpServer({
|
|
19
20
|
name: 'ms-graph-mcp',
|
|
20
|
-
version: '0.1.
|
|
21
|
+
version: '0.1.11',
|
|
21
22
|
});
|
|
22
23
|
|
|
23
24
|
if (isToolEnabled('mail.list_messages')) {
|
|
@@ -66,6 +67,75 @@ export async function startMcpServer(): Promise<void> {
|
|
|
66
67
|
);
|
|
67
68
|
}
|
|
68
69
|
|
|
70
|
+
if (isToolEnabled('onedrive.list_items')) {
|
|
71
|
+
server.tool(
|
|
72
|
+
'onedrive.list_items',
|
|
73
|
+
'List files and folders from OneDrive root or a specific folder',
|
|
74
|
+
{
|
|
75
|
+
folderPath: z.string().trim().optional().describe('Folder path relative to OneDrive root, e.g. /Projects'),
|
|
76
|
+
itemId: z.string().trim().optional().describe('Drive item ID to list children from'),
|
|
77
|
+
top: z.number().int().min(1).max(999).optional().describe('Maximum number of items to return'),
|
|
78
|
+
select: z.string().trim().optional().describe('Optional OData $select clause'),
|
|
79
|
+
},
|
|
80
|
+
async (input) => {
|
|
81
|
+
const token = await getAccessToken();
|
|
82
|
+
const result = await onedrive.listItems(buildGraphClient(token), input);
|
|
83
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }] };
|
|
84
|
+
},
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (isToolEnabled('onedrive.get_item')) {
|
|
89
|
+
server.tool(
|
|
90
|
+
'onedrive.get_item',
|
|
91
|
+
'Get metadata for a OneDrive file or folder by path or item ID',
|
|
92
|
+
{
|
|
93
|
+
path: z.string().trim().optional().describe('Item path relative to OneDrive root, e.g. /Projects/report.docx'),
|
|
94
|
+
itemId: z.string().trim().optional().describe('Drive item ID'),
|
|
95
|
+
select: z.string().trim().optional().describe('Optional OData $select clause'),
|
|
96
|
+
},
|
|
97
|
+
async (input) => {
|
|
98
|
+
const token = await getAccessToken();
|
|
99
|
+
const result = await onedrive.getItem(buildGraphClient(token), input);
|
|
100
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }] };
|
|
101
|
+
},
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (isToolEnabled('onedrive.search_items')) {
|
|
106
|
+
server.tool(
|
|
107
|
+
'onedrive.search_items',
|
|
108
|
+
'Search OneDrive for files and folders',
|
|
109
|
+
{
|
|
110
|
+
query: z.string().trim().min(1).describe('Search query'),
|
|
111
|
+
top: z.number().int().min(1).max(999).optional().describe('Maximum number of items to return'),
|
|
112
|
+
select: z.string().trim().optional().describe('Optional OData $select clause'),
|
|
113
|
+
},
|
|
114
|
+
async (input) => {
|
|
115
|
+
const token = await getAccessToken();
|
|
116
|
+
const result = await onedrive.searchItems(buildGraphClient(token), input);
|
|
117
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }] };
|
|
118
|
+
},
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (isToolEnabled('onedrive.create_folder')) {
|
|
123
|
+
server.tool(
|
|
124
|
+
'onedrive.create_folder',
|
|
125
|
+
'Create a folder in OneDrive root or under a specific parent',
|
|
126
|
+
{
|
|
127
|
+
name: z.string().trim().min(1).describe('Folder name'),
|
|
128
|
+
parentPath: z.string().trim().optional().describe('Parent folder path relative to OneDrive root'),
|
|
129
|
+
parentItemId: z.string().trim().optional().describe('Parent drive item ID'),
|
|
130
|
+
},
|
|
131
|
+
async (input) => {
|
|
132
|
+
const token = await getAccessToken();
|
|
133
|
+
const result = await onedrive.createFolder(buildGraphClient(token), input);
|
|
134
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }] };
|
|
135
|
+
},
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
69
139
|
const transport = new StdioServerTransport();
|
|
70
140
|
await server.connect(transport);
|
|
71
141
|
log('MCP server started (JSON-RPC 2.0 over stdio). Listening for commands...');
|
package/src/tools/calendar.ts
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
import { Client } from '@microsoft/microsoft-graph-client';
|
|
2
|
-
import { log, error } from '../utils.ts';
|
|
2
|
+
import { log, error, describeGraphError } from '../utils.ts';
|
|
3
|
+
|
|
4
|
+
function assertValidEventTimeRange(start: { dateTime: string; timeZone: string }, end: { dateTime: string; timeZone: string }): void {
|
|
5
|
+
const startDate = new Date(start.dateTime);
|
|
6
|
+
const endDate = new Date(end.dateTime);
|
|
7
|
+
|
|
8
|
+
if (Number.isNaN(startDate.getTime())) {
|
|
9
|
+
throw new Error(`Invalid start.dateTime value: ${start.dateTime}`);
|
|
10
|
+
}
|
|
11
|
+
if (Number.isNaN(endDate.getTime())) {
|
|
12
|
+
throw new Error(`Invalid end.dateTime value: ${end.dateTime}`);
|
|
13
|
+
}
|
|
14
|
+
if (endDate.getTime() <= startDate.getTime()) {
|
|
15
|
+
throw new Error('Event end time must be after the start time.');
|
|
16
|
+
}
|
|
17
|
+
if (!start.timeZone.trim() || !end.timeZone.trim()) {
|
|
18
|
+
throw new Error('Both start.timeZone and end.timeZone are required.');
|
|
19
|
+
}
|
|
20
|
+
}
|
|
3
21
|
|
|
4
22
|
export async function createEvent(graphClient: Client, input: {
|
|
5
23
|
subject: string;
|
|
@@ -10,6 +28,8 @@ export async function createEvent(graphClient: Client, input: {
|
|
|
10
28
|
location?: string;
|
|
11
29
|
}): Promise<any> {
|
|
12
30
|
try {
|
|
31
|
+
assertValidEventTimeRange(input.start, input.end);
|
|
32
|
+
|
|
13
33
|
const event = {
|
|
14
34
|
subject: input.subject,
|
|
15
35
|
start: input.start,
|
|
@@ -31,11 +51,6 @@ export async function createEvent(graphClient: Client, input: {
|
|
|
31
51
|
};
|
|
32
52
|
} catch (err: any) {
|
|
33
53
|
error('Error creating calendar event:', err);
|
|
34
|
-
|
|
35
|
-
id: null,
|
|
36
|
-
webLink: null,
|
|
37
|
-
status: 'failed',
|
|
38
|
-
errorMessage: err.message,
|
|
39
|
-
};
|
|
54
|
+
throw new Error(`Failed to create calendar event: ${describeGraphError(err)}`);
|
|
40
55
|
}
|
|
41
56
|
}
|
package/src/tools/index.ts
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import * as mail from './mail.ts';
|
|
2
2
|
import * as calendar from './calendar.ts';
|
|
3
|
-
|
|
3
|
+
import * as onedrive from './onedrive.ts';
|
|
4
4
|
|
|
5
5
|
export const tools = {
|
|
6
6
|
'mail.list_messages': mail.listMessages,
|
|
7
7
|
'calendar.create_event': calendar.createEvent,
|
|
8
|
-
|
|
8
|
+
'onedrive.list_items': onedrive.listItems,
|
|
9
|
+
'onedrive.get_item': onedrive.getItem,
|
|
10
|
+
'onedrive.search_items': onedrive.searchItems,
|
|
11
|
+
'onedrive.create_folder': onedrive.createFolder,
|
|
9
12
|
};
|
package/src/tools/mail.ts
CHANGED
|
@@ -1,10 +1,36 @@
|
|
|
1
1
|
import { Client } from '@microsoft/microsoft-graph-client';
|
|
2
|
-
import { log, error } from '../utils.ts';
|
|
2
|
+
import { log, error, describeGraphError } from '../utils.ts';
|
|
3
|
+
|
|
4
|
+
const WELL_KNOWN_FOLDERS = new Map<string, string>([
|
|
5
|
+
['inbox', 'inbox'],
|
|
6
|
+
['sent', 'sentitems'],
|
|
7
|
+
['sentitems', 'sentitems'],
|
|
8
|
+
['drafts', 'drafts'],
|
|
9
|
+
['deleted', 'deleteditems'],
|
|
10
|
+
['deleteditems', 'deleteditems'],
|
|
11
|
+
['archive', 'archive'],
|
|
12
|
+
['junk', 'junkemail'],
|
|
13
|
+
['junkemail', 'junkemail'],
|
|
14
|
+
['outbox', 'outbox'],
|
|
15
|
+
]);
|
|
16
|
+
|
|
17
|
+
function normalizeFolderId(folderId?: string): string | undefined {
|
|
18
|
+
if (!folderId) {
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const trimmed = folderId.trim();
|
|
23
|
+
if (!trimmed) {
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return WELL_KNOWN_FOLDERS.get(trimmed.toLowerCase()) ?? trimmed;
|
|
28
|
+
}
|
|
3
29
|
|
|
4
30
|
export async function listMessages(graphClient: Client, input: { folderId?: string; top?: number; filter?: string }): Promise<any> {
|
|
5
31
|
try {
|
|
6
|
-
|
|
7
|
-
'/me/messages');
|
|
32
|
+
const folderId = normalizeFolderId(input.folderId);
|
|
33
|
+
let request = graphClient.api(folderId ? `/me/mailFolders/${folderId}/messages` : '/me/messages');
|
|
8
34
|
|
|
9
35
|
if (input.top) {
|
|
10
36
|
request = request.top(input.top);
|
|
@@ -29,6 +55,6 @@ export async function listMessages(graphClient: Client, input: { folderId?: stri
|
|
|
29
55
|
};
|
|
30
56
|
} catch (err: any) {
|
|
31
57
|
error('Error listing messages:', err);
|
|
32
|
-
throw err;
|
|
58
|
+
throw new Error(`Failed to list messages: ${describeGraphError(err)}`);
|
|
33
59
|
}
|
|
34
60
|
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { Client } from '@microsoft/microsoft-graph-client';
|
|
2
|
+
import { error, log, describeGraphError } from '../utils.ts';
|
|
3
|
+
|
|
4
|
+
type DriveItem = {
|
|
5
|
+
id: string;
|
|
6
|
+
name: string;
|
|
7
|
+
webUrl?: string;
|
|
8
|
+
size?: number;
|
|
9
|
+
createdDateTime?: string;
|
|
10
|
+
lastModifiedDateTime?: string;
|
|
11
|
+
folder?: unknown;
|
|
12
|
+
file?: unknown;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
function trimLeadingSlash(value: string): string {
|
|
16
|
+
return value.replace(/^\/+/, '');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function toRootPath(path: string): string {
|
|
20
|
+
const normalized = trimLeadingSlash(path.trim());
|
|
21
|
+
const segments = normalized
|
|
22
|
+
.split('/')
|
|
23
|
+
.filter((segment) => segment.length > 0)
|
|
24
|
+
.map((segment) => encodeURIComponent(segment));
|
|
25
|
+
return `/${segments.join('/')}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function buildItemPath(itemId?: string, folderPath?: string): string {
|
|
29
|
+
if (itemId) {
|
|
30
|
+
return `/me/drive/items/${itemId}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (folderPath) {
|
|
34
|
+
return `/me/drive/root:${toRootPath(folderPath)}:`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return '/me/drive/root';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function simplifyItem(item: DriveItem): Record<string, unknown> {
|
|
41
|
+
return {
|
|
42
|
+
id: item.id,
|
|
43
|
+
name: item.name,
|
|
44
|
+
webUrl: item.webUrl,
|
|
45
|
+
size: item.size,
|
|
46
|
+
createdDateTime: item.createdDateTime,
|
|
47
|
+
lastModifiedDateTime: item.lastModifiedDateTime,
|
|
48
|
+
isFolder: Boolean(item.folder),
|
|
49
|
+
isFile: Boolean(item.file),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function listItems(
|
|
54
|
+
graphClient: Client,
|
|
55
|
+
input: { folderPath?: string; itemId?: string; top?: number; select?: string },
|
|
56
|
+
): Promise<any> {
|
|
57
|
+
try {
|
|
58
|
+
const path = buildItemPath(input.itemId, input.folderPath);
|
|
59
|
+
let request = graphClient.api(`${path}/children`);
|
|
60
|
+
|
|
61
|
+
if (input.top) {
|
|
62
|
+
request = request.top(input.top);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const select = input.select?.trim();
|
|
66
|
+
const response = select
|
|
67
|
+
? await request.select(select).get()
|
|
68
|
+
: await request.get();
|
|
69
|
+
|
|
70
|
+
log(`Listed ${response.value.length} OneDrive items.`);
|
|
71
|
+
return {
|
|
72
|
+
items: response.value.map((item: DriveItem) => simplifyItem(item)),
|
|
73
|
+
nextLink: response['@odata.nextLink'],
|
|
74
|
+
};
|
|
75
|
+
} catch (err: any) {
|
|
76
|
+
error('Error listing OneDrive items:', err);
|
|
77
|
+
throw new Error(`Failed to list OneDrive items: ${describeGraphError(err)}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function getItem(
|
|
82
|
+
graphClient: Client,
|
|
83
|
+
input: { itemId?: string; path?: string; select?: string },
|
|
84
|
+
): Promise<any> {
|
|
85
|
+
try {
|
|
86
|
+
const path = buildItemPath(input.itemId, input.path);
|
|
87
|
+
const select = input.select?.trim();
|
|
88
|
+
const response = select
|
|
89
|
+
? await graphClient.api(path).select(select).get()
|
|
90
|
+
: await graphClient.api(path).get();
|
|
91
|
+
|
|
92
|
+
log(`Fetched OneDrive item: ${response.name ?? response.id}`);
|
|
93
|
+
return simplifyItem(response);
|
|
94
|
+
} catch (err: any) {
|
|
95
|
+
error('Error retrieving OneDrive item:', err);
|
|
96
|
+
throw new Error(`Failed to retrieve OneDrive item: ${describeGraphError(err)}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export async function searchItems(
|
|
101
|
+
graphClient: Client,
|
|
102
|
+
input: { query: string; top?: number; select?: string },
|
|
103
|
+
): Promise<any> {
|
|
104
|
+
try {
|
|
105
|
+
let request = graphClient.api(`/me/drive/root/search(q='${input.query.replace(/'/g, "''")}')`);
|
|
106
|
+
|
|
107
|
+
if (input.top) {
|
|
108
|
+
request = request.top(input.top);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const select = input.select?.trim();
|
|
112
|
+
const response = select
|
|
113
|
+
? await request.select(select).get()
|
|
114
|
+
: await request.get();
|
|
115
|
+
|
|
116
|
+
log(`Search returned ${response.value.length} OneDrive items.`);
|
|
117
|
+
return {
|
|
118
|
+
items: response.value.map((item: DriveItem) => simplifyItem(item)),
|
|
119
|
+
nextLink: response['@odata.nextLink'],
|
|
120
|
+
};
|
|
121
|
+
} catch (err: any) {
|
|
122
|
+
error('Error searching OneDrive items:', err);
|
|
123
|
+
throw new Error(`Failed to search OneDrive items: ${describeGraphError(err)}`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export async function createFolder(
|
|
128
|
+
graphClient: Client,
|
|
129
|
+
input: { name: string; parentPath?: string; parentItemId?: string },
|
|
130
|
+
): Promise<any> {
|
|
131
|
+
try {
|
|
132
|
+
const parentPath = buildItemPath(input.parentItemId, input.parentPath);
|
|
133
|
+
const response = await graphClient.api(`${parentPath}/children`).post({
|
|
134
|
+
name: input.name,
|
|
135
|
+
folder: {},
|
|
136
|
+
'@microsoft.graph.conflictBehavior': 'rename',
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
log(`Created OneDrive folder: ${response.name} (ID: ${response.id})`);
|
|
140
|
+
return simplifyItem(response);
|
|
141
|
+
} catch (err: any) {
|
|
142
|
+
error('Error creating OneDrive folder:', err);
|
|
143
|
+
throw new Error(`Failed to create OneDrive folder: ${describeGraphError(err)}`);
|
|
144
|
+
}
|
|
145
|
+
}
|
package/src/utils.ts
CHANGED
|
@@ -18,4 +18,32 @@ export function warn(message: string) {
|
|
|
18
18
|
log(message, 'warn');
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
export function describeGraphError(err: unknown): string {
|
|
22
|
+
if (err instanceof Error) {
|
|
23
|
+
const parts: string[] = [err.message];
|
|
24
|
+
const graphErr = err as Error & {
|
|
25
|
+
code?: string;
|
|
26
|
+
statusCode?: number;
|
|
27
|
+
body?: { error?: { code?: string; message?: string } };
|
|
28
|
+
response?: { status?: number; body?: { error?: { code?: string; message?: string } } };
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const status = graphErr.statusCode ?? graphErr.response?.status;
|
|
32
|
+
const code = graphErr.code ?? graphErr.body?.error?.code ?? graphErr.response?.body?.error?.code;
|
|
33
|
+
const message = graphErr.body?.error?.message ?? graphErr.response?.body?.error?.message;
|
|
34
|
+
|
|
35
|
+
if (status) {
|
|
36
|
+
parts.unshift(`HTTP ${status}`);
|
|
37
|
+
}
|
|
38
|
+
if (code) {
|
|
39
|
+
parts.push(`code=${code}`);
|
|
40
|
+
}
|
|
41
|
+
if (message && message !== err.message) {
|
|
42
|
+
parts.push(message);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return parts.join(' | ');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return String(err);
|
|
49
|
+
}
|