Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 64 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,29 +93,55 @@ TENANT_ID=00000000-0000-0000-0000-000000000000
```
This will open a browser window for you to authenticate with Microsoft and create a `tokens.json` file.

2. **Create MCP config file** (must be done from the cloned repository)
2. **Configure your AI client** (see sections below for Claude Desktop or Cursor)

### Claude Desktop

Add the following to your `claude_desktop_config.json`:

```json
{
"mcpServers": {
"mstodo": {
"command": "node",
"args": [
"/absolute/path/to/todoMCP/build/cli.js"
]
}
}
}
```

> **Important:** Use `build/cli.js` as the entry point, **not** `build/todo-index.js`. The CLI wrapper handles token loading and is the correct entry point for the server.

The configuration file is located at:
- **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
- **Windows**: `%APPDATA%\Claude\claude_desktop_config.json`
- **Linux**: `~/.config/Claude/claude_desktop_config.json`

After saving the config, restart Claude Desktop. The `mstodo` server should appear in the MCP servers list.

The server automatically reads tokens from `tokens.json` in the project directory and refreshes them when they expire, so you do not need to pass tokens as environment variables.

### Cursor

1. **Create MCP config file** (must be done from the cloned repository)
```bash
npm run create-config
```
This creates an `mcp.json` file with your authentication tokens.

3. **Set up the global MCP configuration**
2. **Set up the global MCP configuration**
```bash
# Copy the mcp.json file to your global Cursor configuration directory
cp mcp.json ~/.cursor/mcp-servers.json
```

This makes the Microsoft To Do MCP available across all your Cursor projects.

4. **Start using with your AI assistant**
- In Cursor, you can now use Microsoft To Do commands directly in any project
3. **Start using with your AI assistant**
- Try commands like `auth status` or `list up todos` to get started

The Claude Desktop configuration file is located at:
- **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
- **Windows**: `%APPDATA%\Claude\claude_desktop_config.json`
- **Linux**: `~/.config/Claude/claude_desktop_config.json`

## Available Tools

- `auth-status`: Check your authentication status
Expand All @@ -137,12 +163,35 @@ The Claude Desktop configuration file is located at:
- The API requires proper authentication and permissions
- Rate limits may apply according to Microsoft's policies

## Token Refresh

Access tokens expire after approximately 1 hour. The server handles this automatically:

- When a token expires, the server uses the refresh token to obtain a new access token.
- The refreshed tokens are saved back to `tokens.json`, so they persist across server restarts.
- If an API call returns a 401 error, the server will attempt a token refresh and retry the request.
- Refresh tokens last approximately 90 days. If the refresh token expires, you will need to re-authenticate with `npm run auth`.

For automatic refresh to work, the `.env` file (containing `CLIENT_ID` and `CLIENT_SECRET`) must be in the project root directory alongside the built files.

## Troubleshooting

### Server Exits Immediately (Claude Desktop)

- **Wrong entry point**: Make sure your config points to `build/cli.js`, not `build/todo-index.js`. The `todo-index.js` file has a direct-execution guard that fails on Windows, causing the process to exit without starting the server.

- **Missing build**: Run `npm run build` to compile the TypeScript source before using the server.

### 401 Unauthorized Errors

- **Wrong TENANT_ID for personal accounts**: If you are using a personal Microsoft account (outlook.com, hotmail.com, live.com), you **must** set `TENANT_ID=consumers` in your `.env` file. Using a specific organizational tenant ID will result in 401 errors because your personal To Do data lives in the consumer environment, not in the organizational tenant.

- **Token expired**: Access tokens last ~1 hour. The server refreshes them automatically, but if refresh fails (e.g., missing `.env` file), you will see 401 errors. Check that your `.env` file exists in the project root with valid `CLIENT_ID` and `CLIENT_SECRET`.

### Authentication Issues

- **"MailboxNotEnabledForRESTAPI" error**: This typically means you're using a personal Microsoft account. Microsoft To Do API access is limited for personal accounts through the Graph API.
- **"MailboxNotEnabledForRESTAPI" error**: This typically means you're using a personal Microsoft account with the wrong tenant. Set `TENANT_ID=consumers` in your `.env` file and re-authenticate with `npm run auth`.

- **Token acquisition failures**: Make sure your `CLIENT_ID`, `CLIENT_SECRET`, and `TENANT_ID` are correct in your `.env` file.

- **Permission issues**: Ensure you have granted admin consent for the required permissions in your Azure App registration.
Expand All @@ -151,18 +200,12 @@ The Claude Desktop configuration file is located at:

- **Work/School Accounts**: These typically work best with the To Do API. Use `TENANT_ID=organizations` or your specific tenant ID.

- **Personal Accounts**: These have limited access to the To Do API. If you must use a personal account, try `TENANT_ID=consumers` or `TENANT_ID=common`.
- **Personal Accounts**: Use `TENANT_ID=consumers`. Personal accounts require the consumer endpoint for To Do API access.

### Checking Authentication Status

You can check your authentication status using the `auth-status` tool or by examining the expiration time in your tokens:

```bash
cat tokens.json | grep expiresAt
```

To convert the timestamp to a readable date:
You can check your authentication status using the `auth-status` tool, or by examining `tokens.json`:

```bash
date -r $(echo "$(cat tokens.json | grep expiresAt | cut -d ":" -f2 | cut -d "," -f1) / 1000" | bc)
node -e "const t = require('./tokens.json'); console.log('Expires:', new Date(t.expiresAt).toLocaleString(), t.expiresAt < Date.now() ? '(EXPIRED)' : '(valid)');"
```
27 changes: 18 additions & 9 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,15 @@ const __dirname = path.dirname(__filename);
// Check for tokens in environment variables
let accessToken = process.env.MSTODO_ACCESS_TOKEN;
let refreshToken = process.env.MSTODO_REFRESH_TOKEN;
let expiresAt: number | undefined;

// Define token file path
const TOKEN_FILE_PATH = process.env.MSTODO_TOKEN_FILE ||
path.join(process.cwd(), 'tokens.json');
// Define token file path — prefer env var, then look next to the script, then cwd
const TOKEN_FILE_PATH = process.env.MSTODO_TOKEN_FILE ||
(() => {
const scriptDir = path.join(__dirname, '..', 'tokens.json');
if (fs.existsSync(scriptDir)) return scriptDir;
return path.join(process.cwd(), 'tokens.json');
})();

// Log startup info
console.error('Microsoft Todo MCP CLI');
Expand All @@ -26,28 +31,32 @@ if ((!accessToken || !refreshToken) && fs.existsSync(TOKEN_FILE_PATH)) {
try {
console.error('Reading tokens from file...');
const tokenData = JSON.parse(fs.readFileSync(TOKEN_FILE_PATH, 'utf8'));

// If we found tokens in the file, use them

if (!accessToken && tokenData.accessToken) {
accessToken = tokenData.accessToken;
console.error('Using access token from file');
}

if (!refreshToken && tokenData.refreshToken) {
refreshToken = tokenData.refreshToken;
console.error('Using refresh token from file');
}

if (tokenData.expiresAt) {
expiresAt = tokenData.expiresAt;
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
console.error('Error reading token file:', errorMessage);
}
}

// Start the MCP server with the available tokens
startServer({
accessToken,
startServer({
accessToken,
refreshToken,
tokenFilePath: TOKEN_FILE_PATH
tokenFilePath: TOKEN_FILE_PATH,
expiresAt
}).catch(error => {
const errorMessage = error instanceof Error ? error.message : String(error);
console.error('Error starting server:', errorMessage);
Expand Down
Loading