Clean git move from play
This commit is contained in:
71
.gitignore
vendored
Normal file
71
.gitignore
vendored
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
# Environment variables
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
|
||||||
|
# Dependencies
|
||||||
|
node_modules/
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
|
||||||
|
# Build outputs
|
||||||
|
build/
|
||||||
|
dist/
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# IDE and editor files
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Operating system files
|
||||||
|
Thumbs.db
|
||||||
|
Desktop.ini
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs/
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Testing
|
||||||
|
coverage/
|
||||||
|
.nyc_output/
|
||||||
|
*.lcov
|
||||||
|
|
||||||
|
# Temporary files
|
||||||
|
tmp/
|
||||||
|
temp/
|
||||||
|
*.tmp
|
||||||
|
|
||||||
|
# Package manager lock files (optional - uncomment if you want to ignore them)
|
||||||
|
# package-lock.json
|
||||||
|
# yarn.lock
|
||||||
|
# pnpm-lock.yaml
|
||||||
|
|
||||||
|
# Debug files
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
*.pid.lock
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids/
|
||||||
|
lib-cov/
|
||||||
|
|
||||||
|
# Optional npm cache directory
|
||||||
|
.npm
|
||||||
|
|
||||||
|
# Optional eslint cache
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# Optional REPL history
|
||||||
|
.node_repl_history
|
||||||
|
|
||||||
|
# Output of 'npm pack'
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# Yarn Integrity file
|
||||||
|
.yarn-integrity
|
||||||
164
QUICKSTART.md
Normal file
164
QUICKSTART.md
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
# Quick Start Guide
|
||||||
|
|
||||||
|
## 🚀 Get Started in 5 Minutes
|
||||||
|
|
||||||
|
### Step 1: Get Your Nextcloud App Password
|
||||||
|
|
||||||
|
1. Log into your Nextcloud instance
|
||||||
|
2. Go to **Settings** → **Security**
|
||||||
|
3. Scroll to "Devices & sessions"
|
||||||
|
4. Enter a name (e.g., "MCP Server") and click "Create new app password"
|
||||||
|
5. Copy the generated password
|
||||||
|
|
||||||
|
### Step 2: Configure the Server
|
||||||
|
|
||||||
|
Create a `.env` file in the project root:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
Edit `.env` with your details:
|
||||||
|
|
||||||
|
```env
|
||||||
|
NEXTCLOUD_URL=https://your-nextcloud.com
|
||||||
|
NEXTCLOUD_USERNAME=your-username
|
||||||
|
NEXTCLOUD_PASSWORD=paste-your-app-password-here
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 3: Build and Test
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build the project
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
# Test it works (Ctrl+C to exit)
|
||||||
|
npm run start
|
||||||
|
```
|
||||||
|
|
||||||
|
You should see: `Nextcloud MCP Server running on stdio`
|
||||||
|
|
||||||
|
### Step 4: Connect to Claude Desktop
|
||||||
|
|
||||||
|
Edit your Claude Desktop config:
|
||||||
|
|
||||||
|
**macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
|
||||||
|
**Windows**: `%APPDATA%\Claude\claude_desktop_config.json`
|
||||||
|
|
||||||
|
Add this configuration (replace the path with your actual path):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"nextcloud": {
|
||||||
|
"command": "node",
|
||||||
|
"args": ["/full/path/to/nextcloud-mcp/build/index.js"],
|
||||||
|
"env": {
|
||||||
|
"NEXTCLOUD_URL": "https://your-nextcloud.com",
|
||||||
|
"NEXTCLOUD_USERNAME": "your-username",
|
||||||
|
"NEXTCLOUD_PASSWORD": "your-app-password"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Important**: Use the full absolute path to the build/index.js file!
|
||||||
|
|
||||||
|
### Step 5: Restart Claude Desktop
|
||||||
|
|
||||||
|
Quit Claude Desktop completely and reopen it.
|
||||||
|
|
||||||
|
### Step 6: Test with Claude
|
||||||
|
|
||||||
|
Try these prompts in Claude:
|
||||||
|
|
||||||
|
```
|
||||||
|
"Show me my open tasks"
|
||||||
|
"What meetings do I have today?"
|
||||||
|
"List my recent notes"
|
||||||
|
"Show me my latest emails"
|
||||||
|
```
|
||||||
|
|
||||||
|
## ✅ Verification Checklist
|
||||||
|
|
||||||
|
Before asking Claude to use Nextcloud:
|
||||||
|
|
||||||
|
- [ ] Nextcloud URL is correct (HTTPS, no trailing slash)
|
||||||
|
- [ ] App password is correctly copied (no extra spaces)
|
||||||
|
- [ ] Required apps are installed:
|
||||||
|
- [ ] Tasks app
|
||||||
|
- [ ] Calendar app
|
||||||
|
- [ ] Notes app
|
||||||
|
- [ ] Mail app (if using email features)
|
||||||
|
- [ ] Full absolute path used in Claude Desktop config
|
||||||
|
- [ ] Claude Desktop has been restarted
|
||||||
|
|
||||||
|
## 🔧 Common Issues
|
||||||
|
|
||||||
|
### "Connection refused" or timeout
|
||||||
|
- Check your Nextcloud URL is accessible
|
||||||
|
- Verify you're using HTTPS
|
||||||
|
- Try accessing the URL in your browser
|
||||||
|
|
||||||
|
### "Authentication failed" or 401 error
|
||||||
|
- Regenerate a new app password
|
||||||
|
- Make sure there are no spaces before/after the password
|
||||||
|
- Try your username in lowercase
|
||||||
|
|
||||||
|
### "Calendar not found" or "Tasks not found"
|
||||||
|
- Check that you have at least one calendar created
|
||||||
|
- Verify the Tasks app is installed and has a task list
|
||||||
|
- See customization section in README for different calendar names
|
||||||
|
|
||||||
|
### Claude doesn't show Nextcloud tools
|
||||||
|
- Verify Claude Desktop config file syntax (use a JSON validator)
|
||||||
|
- Check the path to build/index.js is correct and absolute
|
||||||
|
- Look at Claude Desktop logs for errors
|
||||||
|
- Restart Claude Desktop after config changes
|
||||||
|
|
||||||
|
## 🎯 Next Steps
|
||||||
|
|
||||||
|
Once working:
|
||||||
|
1. Customize calendar/task list names in `src/index.ts` if needed
|
||||||
|
2. Add more tools as needed for your workflow
|
||||||
|
3. Check out the full README.md for advanced features
|
||||||
|
|
||||||
|
## 📚 Example Use Cases
|
||||||
|
|
||||||
|
**Task Management**:
|
||||||
|
```
|
||||||
|
"Create a task to review the integration docs, due tomorrow"
|
||||||
|
"Show me all my completed tasks this week"
|
||||||
|
"Mark task X as complete"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Calendar**:
|
||||||
|
```
|
||||||
|
"What's on my calendar tomorrow?"
|
||||||
|
"Schedule a meeting with the team next Monday at 2pm"
|
||||||
|
"Show me my events for next week"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Notes**:
|
||||||
|
```
|
||||||
|
"Create a note with my meeting notes from today"
|
||||||
|
"Show me all my notes"
|
||||||
|
"What's in note ID 123?"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Email**:
|
||||||
|
```
|
||||||
|
"Show me my latest emails"
|
||||||
|
"What are my unread messages?"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🆘 Getting Help
|
||||||
|
|
||||||
|
If you're stuck:
|
||||||
|
1. Check the logs: `~/Library/Logs/Claude/mcp*.log` (macOS)
|
||||||
|
2. Review the full README.md
|
||||||
|
3. Verify all prerequisites are met
|
||||||
|
4. Test the Nextcloud API directly using curl to isolate issues
|
||||||
|
|
||||||
|
Happy automating! 🎉
|
||||||
241
README.md
241
README.md
@@ -1,2 +1,239 @@
|
|||||||
# nextcloud-mcp
|
# Nextcloud MCP Server
|
||||||
MCP server for nextcloud
|
|
||||||
|
A Model Context Protocol (MCP) server that integrates with Nextcloud to provide access to:
|
||||||
|
- **Tasks** (via CalDAV)
|
||||||
|
- **Calendar Events** (via CalDAV)
|
||||||
|
- **Notes** (via Notes API)
|
||||||
|
- **Emails** (via Mail API)
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
### Tasks
|
||||||
|
- ✅ Get tasks (filter by status: all/open/completed)
|
||||||
|
- ✅ Create new tasks with due dates and priorities
|
||||||
|
- ✅ Update tasks (mark complete, change summary, etc.)
|
||||||
|
|
||||||
|
### Calendar
|
||||||
|
- ✅ Get calendar events with date range filtering
|
||||||
|
- ✅ Create new calendar events with details and location
|
||||||
|
|
||||||
|
### Notes
|
||||||
|
- ✅ Get all notes
|
||||||
|
- ✅ Create new notes with markdown support
|
||||||
|
- ✅ Get specific note content by ID
|
||||||
|
|
||||||
|
### Email
|
||||||
|
- ✅ Get emails from inbox
|
||||||
|
- 📧 Requires Nextcloud Mail app configured
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
1. **Nextcloud Instance** (any recent version with CalDAV support)
|
||||||
|
2. **Required Nextcloud Apps**:
|
||||||
|
- Tasks (for task management)
|
||||||
|
- Calendar (for events)
|
||||||
|
- Notes (for note-taking)
|
||||||
|
- Mail (optional, for email access)
|
||||||
|
|
||||||
|
3. **App Password**: Generate in Nextcloud Settings > Security > Devices & sessions
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
1. Copy the environment template:
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Edit `.env` with your Nextcloud credentials:
|
||||||
|
```env
|
||||||
|
NEXTCLOUD_URL=https://your-nextcloud.com
|
||||||
|
NEXTCLOUD_USERNAME=your-username
|
||||||
|
NEXTCLOUD_PASSWORD=your-app-password
|
||||||
|
```
|
||||||
|
|
||||||
|
⚠️ **Important**: Always use an app password, never your main Nextcloud password!
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Testing Locally
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Development mode (auto-reload)
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# Production mode
|
||||||
|
npm run start
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using with Claude Desktop
|
||||||
|
|
||||||
|
Add this to your Claude Desktop configuration file:
|
||||||
|
|
||||||
|
**macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
|
||||||
|
**Windows**: `%APPDATA%\Claude\claude_desktop_config.json`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"nextcloud": {
|
||||||
|
"command": "node",
|
||||||
|
"args": ["/path/to/nextcloud-mcp/build/index.js"],
|
||||||
|
"env": {
|
||||||
|
"NEXTCLOUD_URL": "https://your-nextcloud.com",
|
||||||
|
"NEXTCLOUD_USERNAME": "your-username",
|
||||||
|
"NEXTCLOUD_PASSWORD": "your-app-password"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Or use the development version with tsx:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"nextcloud": {
|
||||||
|
"command": "npx",
|
||||||
|
"args": ["-y", "tsx", "/path/to/nextcloud-mcp/src/index.ts"],
|
||||||
|
"env": {
|
||||||
|
"NEXTCLOUD_URL": "https://your-nextcloud.com",
|
||||||
|
"NEXTCLOUD_USERNAME": "your-username",
|
||||||
|
"NEXTCLOUD_PASSWORD": "your-app-password"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Restart Claude Desktop to load the MCP server.
|
||||||
|
|
||||||
|
## Available Tools
|
||||||
|
|
||||||
|
Once connected, Claude can use these tools:
|
||||||
|
|
||||||
|
### Tasks
|
||||||
|
- `get_tasks` - Retrieve tasks (filter by status, limit results)
|
||||||
|
- `create_task` - Create new task with summary, description, due date, priority
|
||||||
|
- `update_task` - Update existing task (mark complete, change details)
|
||||||
|
|
||||||
|
### Calendar
|
||||||
|
- `get_calendar_events` - Get events in date range
|
||||||
|
- `create_calendar_event` - Create new event with details
|
||||||
|
|
||||||
|
### Notes
|
||||||
|
- `get_notes` - List all notes
|
||||||
|
- `create_note` - Create new note with markdown
|
||||||
|
- `get_note_content` - Get full content of specific note
|
||||||
|
|
||||||
|
### Email
|
||||||
|
- `get_emails` - Retrieve recent emails from inbox
|
||||||
|
|
||||||
|
## Example Prompts for Claude
|
||||||
|
|
||||||
|
Once the MCP server is connected, you can ask Claude:
|
||||||
|
|
||||||
|
```
|
||||||
|
"Show me my open tasks for this week"
|
||||||
|
"Create a task to review the Q4 report, due next Friday"
|
||||||
|
"What meetings do I have tomorrow?"
|
||||||
|
"Create a calendar event for team standup tomorrow at 10am"
|
||||||
|
"Show me my recent notes"
|
||||||
|
"Create a note about the meeting outcomes"
|
||||||
|
"What are my latest emails?"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Connection Issues
|
||||||
|
- Verify your Nextcloud URL (use HTTPS, no trailing slash)
|
||||||
|
- Ensure app password is correct
|
||||||
|
- Check that required apps (Tasks, Calendar, Notes) are installed
|
||||||
|
|
||||||
|
### CalDAV Issues
|
||||||
|
- Verify calendar/task list names match your Nextcloud setup
|
||||||
|
- Default calendar name is "personal" - adjust in code if needed
|
||||||
|
- Default task list name is "tasks" - adjust in code if needed
|
||||||
|
|
||||||
|
### Email Issues
|
||||||
|
- Ensure Nextcloud Mail app is installed and configured
|
||||||
|
- Check that at least one email account is set up
|
||||||
|
- Account ID defaults to 0 (first account)
|
||||||
|
|
||||||
|
### Debug Mode
|
||||||
|
Check the MCP server logs in Claude Desktop:
|
||||||
|
- **macOS**: `~/Library/Logs/Claude/mcp*.log`
|
||||||
|
- **Windows**: `%APPDATA%\Claude\logs\mcp*.log`
|
||||||
|
|
||||||
|
## API Endpoints Used
|
||||||
|
|
||||||
|
- CalDAV: `/remote.php/dav/calendars/{username}/`
|
||||||
|
- Notes: `/index.php/apps/notes/api/v1/notes`
|
||||||
|
- Mail: `/index.php/apps/mail/api/`
|
||||||
|
|
||||||
|
## Security Notes
|
||||||
|
|
||||||
|
- 🔐 Always use app passwords, never your main password
|
||||||
|
- 🔒 Store credentials securely (environment variables, not in code)
|
||||||
|
- 🛡️ Use HTTPS for your Nextcloud instance
|
||||||
|
- 🔑 Limit app password scopes if possible in Nextcloud
|
||||||
|
|
||||||
|
## Customization
|
||||||
|
|
||||||
|
### Changing Calendar Names
|
||||||
|
Edit `src/index.ts` and update the calendar paths:
|
||||||
|
```typescript
|
||||||
|
// Default personal calendar
|
||||||
|
const caldavPath = `/remote.php/dav/calendars/${this.config.username}/personal/`;
|
||||||
|
|
||||||
|
// For a different calendar, change "personal" to your calendar name
|
||||||
|
const caldavPath = `/remote.php/dav/calendars/${this.config.username}/work/`;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Changing Task List Names
|
||||||
|
```typescript
|
||||||
|
// Default tasks list
|
||||||
|
const caldavPath = `/remote.php/dav/calendars/${this.config.username}/tasks/`;
|
||||||
|
|
||||||
|
// For a different task list
|
||||||
|
const caldavPath = `/remote.php/dav/calendars/${this.config.username}/personal-tasks/`;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install dependencies
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# Run in development mode with auto-reload
|
||||||
|
npm run watch # In one terminal
|
||||||
|
npm run dev # In another terminal
|
||||||
|
|
||||||
|
# Build for production
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
Feel free to submit issues and pull requests for:
|
||||||
|
- Additional Nextcloud integrations
|
||||||
|
- Bug fixes
|
||||||
|
- Documentation improvements
|
||||||
|
- Feature enhancements
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
|
||||||
|
- [MCP Documentation](https://docs.anthropic.com/mcp)
|
||||||
|
- [Nextcloud API Documentation](https://docs.nextcloud.com/server/latest/developer_manual/)
|
||||||
|
- [CalDAV Documentation](https://www.rfc-editor.org/rfc/rfc4791)
|
||||||
|
|||||||
1792
package-lock.json
generated
Normal file
1792
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
30
package.json
Normal file
30
package.json
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"name": "nextcloud-mcp",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "MCP server for Nextcloud integration - tasks, calendar, notes, and email",
|
||||||
|
"type": "module",
|
||||||
|
"bin": {
|
||||||
|
"nextcloud-mcp": "./build/index.js"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc",
|
||||||
|
"watch": "tsc --watch",
|
||||||
|
"start": "node build/index.js",
|
||||||
|
"dev": "tsx src/index.ts",
|
||||||
|
"test-connection": "node test-connection.js"
|
||||||
|
},
|
||||||
|
"keywords": ["mcp", "nextcloud", "calendar", "tasks", "notes", "email"],
|
||||||
|
"author": "",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@modelcontextprotocol/sdk": "^1.20.2",
|
||||||
|
"axios": "^1.12.2",
|
||||||
|
"date-fns": "^4.1.0",
|
||||||
|
"zod": "^3.25.76"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^24.9.1",
|
||||||
|
"tsx": "^4.20.6",
|
||||||
|
"typescript": "^5.9.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
892
src/index.ts
Normal file
892
src/index.ts
Normal file
@@ -0,0 +1,892 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
||||||
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||||
|
import {
|
||||||
|
CallToolRequestSchema,
|
||||||
|
ListToolsRequestSchema,
|
||||||
|
Tool,
|
||||||
|
} from "@modelcontextprotocol/sdk/types.js";
|
||||||
|
import axios, { AxiosInstance } from "axios";
|
||||||
|
import { parseISO, format } from "date-fns";
|
||||||
|
|
||||||
|
interface NextcloudConfig {
|
||||||
|
url: string;
|
||||||
|
username: string;
|
||||||
|
password: string; // App password recommended
|
||||||
|
}
|
||||||
|
|
||||||
|
class NextcloudMCPServer {
|
||||||
|
private server: Server;
|
||||||
|
private axiosInstance: AxiosInstance;
|
||||||
|
private config: NextcloudConfig;
|
||||||
|
|
||||||
|
constructor(config: NextcloudConfig) {
|
||||||
|
this.config = config;
|
||||||
|
this.server = new Server(
|
||||||
|
{
|
||||||
|
name: "nextcloud-mcp-server",
|
||||||
|
version: "1.0.0",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
capabilities: {
|
||||||
|
tools: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create axios instance with basic auth
|
||||||
|
this.axiosInstance = axios.create({
|
||||||
|
baseURL: config.url,
|
||||||
|
auth: {
|
||||||
|
username: config.username,
|
||||||
|
password: config.password,
|
||||||
|
},
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/xml",
|
||||||
|
"Accept": "application/xml",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
this.setupHandlers();
|
||||||
|
this.setupErrorHandling();
|
||||||
|
}
|
||||||
|
|
||||||
|
private setupErrorHandling(): void {
|
||||||
|
this.server.onerror = (error) => {
|
||||||
|
console.error("[MCP Error]", error);
|
||||||
|
};
|
||||||
|
|
||||||
|
process.on("SIGINT", async () => {
|
||||||
|
await this.server.close();
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private setupHandlers(): void {
|
||||||
|
// List available tools
|
||||||
|
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||||
|
return {
|
||||||
|
tools: this.getTools(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle tool calls
|
||||||
|
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||||
|
const { name, arguments: args } = request.params;
|
||||||
|
|
||||||
|
try {
|
||||||
|
switch (name) {
|
||||||
|
case "get_tasks":
|
||||||
|
return await this.getTasks(args as any);
|
||||||
|
case "create_task":
|
||||||
|
return await this.createTask(args as any);
|
||||||
|
case "update_task":
|
||||||
|
return await this.updateTask(args as any);
|
||||||
|
case "get_calendar_events":
|
||||||
|
return await this.getCalendarEvents(args as any);
|
||||||
|
case "create_calendar_event":
|
||||||
|
return await this.createCalendarEvent(args as any);
|
||||||
|
case "get_notes":
|
||||||
|
return await this.getNotes(args as any);
|
||||||
|
case "create_note":
|
||||||
|
return await this.createNote(args as any);
|
||||||
|
case "get_note_content":
|
||||||
|
return await this.getNoteContent(args as any);
|
||||||
|
case "get_emails":
|
||||||
|
return await this.getEmails(args as any);
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown tool: ${name}`);
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: `Error: ${error.message}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
isError: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private getTools(): Tool[] {
|
||||||
|
return [
|
||||||
|
// Tasks tools
|
||||||
|
{
|
||||||
|
name: "get_tasks",
|
||||||
|
description:
|
||||||
|
"Retrieve tasks from Nextcloud. Can filter by status (completed/open) and limit results.",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
status: {
|
||||||
|
type: "string",
|
||||||
|
enum: ["all", "open", "completed"],
|
||||||
|
description: "Filter tasks by status",
|
||||||
|
default: "all",
|
||||||
|
},
|
||||||
|
limit: {
|
||||||
|
type: "number",
|
||||||
|
description: "Maximum number of tasks to return",
|
||||||
|
default: 50,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "create_task",
|
||||||
|
description: "Create a new task in Nextcloud",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
summary: {
|
||||||
|
type: "string",
|
||||||
|
description: "Task title/summary",
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
type: "string",
|
||||||
|
description: "Task description (optional)",
|
||||||
|
},
|
||||||
|
due: {
|
||||||
|
type: "string",
|
||||||
|
description: "Due date in ISO format (YYYY-MM-DD) (optional)",
|
||||||
|
},
|
||||||
|
priority: {
|
||||||
|
type: "number",
|
||||||
|
description: "Priority (1-9, where 1 is highest) (optional)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ["summary"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "update_task",
|
||||||
|
description: "Update an existing task (mark as complete, change summary, etc.)",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
taskId: {
|
||||||
|
type: "string",
|
||||||
|
description: "Task ID/UID",
|
||||||
|
},
|
||||||
|
summary: {
|
||||||
|
type: "string",
|
||||||
|
description: "New task title/summary (optional)",
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
type: "string",
|
||||||
|
enum: ["NEEDS-ACTION", "IN-PROCESS", "COMPLETED", "CANCELLED"],
|
||||||
|
description: "New task status (optional)",
|
||||||
|
},
|
||||||
|
percentComplete: {
|
||||||
|
type: "number",
|
||||||
|
description: "Completion percentage 0-100 (optional)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ["taskId"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Calendar tools
|
||||||
|
{
|
||||||
|
name: "get_calendar_events",
|
||||||
|
description:
|
||||||
|
"Retrieve calendar events from Nextcloud. Can specify date range.",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
startDate: {
|
||||||
|
type: "string",
|
||||||
|
description:
|
||||||
|
"Start date in ISO format (YYYY-MM-DD). Defaults to today.",
|
||||||
|
},
|
||||||
|
endDate: {
|
||||||
|
type: "string",
|
||||||
|
description:
|
||||||
|
"End date in ISO format (YYYY-MM-DD). Defaults to 30 days from start.",
|
||||||
|
},
|
||||||
|
limit: {
|
||||||
|
type: "number",
|
||||||
|
description: "Maximum number of events to return",
|
||||||
|
default: 50,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "create_calendar_event",
|
||||||
|
description: "Create a new calendar event in Nextcloud",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
summary: {
|
||||||
|
type: "string",
|
||||||
|
description: "Event title/summary",
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
type: "string",
|
||||||
|
description: "Event description (optional)",
|
||||||
|
},
|
||||||
|
startDateTime: {
|
||||||
|
type: "string",
|
||||||
|
description: "Start date/time in ISO format (YYYY-MM-DDTHH:mm:ss)",
|
||||||
|
},
|
||||||
|
endDateTime: {
|
||||||
|
type: "string",
|
||||||
|
description: "End date/time in ISO format (YYYY-MM-DDTHH:mm:ss)",
|
||||||
|
},
|
||||||
|
location: {
|
||||||
|
type: "string",
|
||||||
|
description: "Event location (optional)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ["summary", "startDateTime", "endDateTime"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Notes tools
|
||||||
|
{
|
||||||
|
name: "get_notes",
|
||||||
|
description: "Retrieve all notes from Nextcloud Notes app",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
limit: {
|
||||||
|
type: "number",
|
||||||
|
description: "Maximum number of notes to return",
|
||||||
|
default: 50,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "create_note",
|
||||||
|
description: "Create a new note in Nextcloud Notes app",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
title: {
|
||||||
|
type: "string",
|
||||||
|
description: "Note title (first line)",
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
type: "string",
|
||||||
|
description: "Note content (markdown supported)",
|
||||||
|
},
|
||||||
|
category: {
|
||||||
|
type: "string",
|
||||||
|
description: "Note category/folder (optional)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ["content"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "get_note_content",
|
||||||
|
description: "Get the full content of a specific note by ID",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
noteId: {
|
||||||
|
type: "number",
|
||||||
|
description: "Note ID",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ["noteId"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Email tools
|
||||||
|
{
|
||||||
|
name: "get_emails",
|
||||||
|
description:
|
||||||
|
"Retrieve emails from Nextcloud Mail app. Returns recent emails from inbox.",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
accountId: {
|
||||||
|
type: "number",
|
||||||
|
description: "Mail account ID (use 0 for default)",
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
limit: {
|
||||||
|
type: "number",
|
||||||
|
description: "Maximum number of emails to return",
|
||||||
|
default: 20,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== TASKS METHODS ==========
|
||||||
|
private async getTasks(args: any) {
|
||||||
|
const status = args.status || "all";
|
||||||
|
const limit = args.limit || 50;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// CalDAV REPORT request to get tasks
|
||||||
|
const caldavPath = `/remote.php/dav/calendars/${this.config.username}/tasks/`;
|
||||||
|
|
||||||
|
const requestBody = `<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<c:calendar-query xmlns:c="urn:ietf:params:xml:ns:caldav" xmlns:d="DAV:">
|
||||||
|
<d:prop>
|
||||||
|
<d:getetag />
|
||||||
|
<c:calendar-data />
|
||||||
|
</d:prop>
|
||||||
|
<c:filter>
|
||||||
|
<c:comp-filter name="VCALENDAR">
|
||||||
|
<c:comp-filter name="VTODO" />
|
||||||
|
</c:comp-filter>
|
||||||
|
</c:filter>
|
||||||
|
</c:calendar-query>`;
|
||||||
|
|
||||||
|
const response = await this.axiosInstance.request({
|
||||||
|
method: "REPORT",
|
||||||
|
url: caldavPath,
|
||||||
|
data: requestBody,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/xml",
|
||||||
|
Depth: "1",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const tasks = this.parseTasksFromCalDAV(response.data, status, limit);
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: JSON.stringify(tasks, null, 2),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
} catch (error: any) {
|
||||||
|
throw new Error(`Failed to fetch tasks: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseTasksFromCalDAV(
|
||||||
|
xmlData: string,
|
||||||
|
status: string,
|
||||||
|
limit: number
|
||||||
|
): any[] {
|
||||||
|
const tasks: any[] = [];
|
||||||
|
|
||||||
|
// Basic XML parsing for VTODO components
|
||||||
|
const todoMatches = xmlData.matchAll(
|
||||||
|
/<c:calendar-data[^>]*>([\s\S]*?)<\/c:calendar-data>/g
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const match of todoMatches) {
|
||||||
|
if (tasks.length >= limit) break;
|
||||||
|
|
||||||
|
const todoData = match[1];
|
||||||
|
const task = this.parseVTODO(todoData);
|
||||||
|
|
||||||
|
if (task) {
|
||||||
|
if (status === "all") {
|
||||||
|
tasks.push(task);
|
||||||
|
} else if (
|
||||||
|
status === "completed" &&
|
||||||
|
task.status === "COMPLETED"
|
||||||
|
) {
|
||||||
|
tasks.push(task);
|
||||||
|
} else if (
|
||||||
|
status === "open" &&
|
||||||
|
task.status !== "COMPLETED"
|
||||||
|
) {
|
||||||
|
tasks.push(task);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return tasks;
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseVTODO(todoData: string): any | null {
|
||||||
|
const lines = todoData.split(/\r?\n/);
|
||||||
|
const task: any = {};
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
if (line.startsWith("UID:")) {
|
||||||
|
task.uid = line.substring(4).trim();
|
||||||
|
} else if (line.startsWith("SUMMARY:")) {
|
||||||
|
task.summary = line.substring(8).trim();
|
||||||
|
} else if (line.startsWith("STATUS:")) {
|
||||||
|
task.status = line.substring(7).trim();
|
||||||
|
} else if (line.startsWith("PERCENT-COMPLETE:")) {
|
||||||
|
task.percentComplete = parseInt(line.substring(17).trim());
|
||||||
|
} else if (line.startsWith("DUE")) {
|
||||||
|
const dueMatch = line.match(/DUE[^:]*:(\d{8}T?\d{6}Z?)/);
|
||||||
|
if (dueMatch) {
|
||||||
|
task.due = this.parseICalDate(dueMatch[1]);
|
||||||
|
}
|
||||||
|
} else if (line.startsWith("PRIORITY:")) {
|
||||||
|
task.priority = parseInt(line.substring(9).trim());
|
||||||
|
} else if (line.startsWith("DESCRIPTION:")) {
|
||||||
|
task.description = line.substring(12).trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return task.uid ? task : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async createTask(args: any) {
|
||||||
|
const { summary, description, due, priority } = args;
|
||||||
|
const uid = this.generateUID();
|
||||||
|
|
||||||
|
let vtodo = `BEGIN:VCALENDAR
|
||||||
|
VERSION:2.0
|
||||||
|
PRODID:-//Nextcloud MCP Server//EN
|
||||||
|
BEGIN:VTODO
|
||||||
|
UID:${uid}
|
||||||
|
SUMMARY:${summary}
|
||||||
|
STATUS:NEEDS-ACTION
|
||||||
|
CREATED:${this.formatICalDateTime(new Date())}`;
|
||||||
|
|
||||||
|
if (description) {
|
||||||
|
vtodo += `\nDESCRIPTION:${description}`;
|
||||||
|
}
|
||||||
|
if (due) {
|
||||||
|
vtodo += `\nDUE:${this.formatICalDate(new Date(due))}`;
|
||||||
|
}
|
||||||
|
if (priority) {
|
||||||
|
vtodo += `\nPRIORITY:${priority}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
vtodo += `\nEND:VTODO
|
||||||
|
END:VCALENDAR`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const caldavPath = `/remote.php/dav/calendars/${this.config.username}/tasks/${uid}.ics`;
|
||||||
|
|
||||||
|
await this.axiosInstance.put(caldavPath, vtodo, {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "text/calendar",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: `Task created successfully with UID: ${uid}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
} catch (error: any) {
|
||||||
|
throw new Error(`Failed to create task: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async updateTask(args: any) {
|
||||||
|
const { taskId, summary, status, percentComplete } = args;
|
||||||
|
|
||||||
|
// First, fetch the existing task
|
||||||
|
const caldavPath = `/remote.php/dav/calendars/${this.config.username}/tasks/${taskId}.ics`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await this.axiosInstance.get(caldavPath);
|
||||||
|
let vtodo = response.data;
|
||||||
|
|
||||||
|
// Update fields
|
||||||
|
if (summary) {
|
||||||
|
vtodo = vtodo.replace(/SUMMARY:.*/, `SUMMARY:${summary}`);
|
||||||
|
}
|
||||||
|
if (status) {
|
||||||
|
vtodo = vtodo.replace(/STATUS:.*/, `STATUS:${status}`);
|
||||||
|
}
|
||||||
|
if (percentComplete !== undefined) {
|
||||||
|
if (vtodo.includes("PERCENT-COMPLETE:")) {
|
||||||
|
vtodo = vtodo.replace(
|
||||||
|
/PERCENT-COMPLETE:.*/,
|
||||||
|
`PERCENT-COMPLETE:${percentComplete}`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
vtodo = vtodo.replace(
|
||||||
|
/END:VTODO/,
|
||||||
|
`PERCENT-COMPLETE:${percentComplete}\nEND:VTODO`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update LAST-MODIFIED
|
||||||
|
vtodo = vtodo.replace(
|
||||||
|
/LAST-MODIFIED:.*/,
|
||||||
|
`LAST-MODIFIED:${this.formatICalDateTime(new Date())}`
|
||||||
|
);
|
||||||
|
|
||||||
|
await this.axiosInstance.put(caldavPath, vtodo, {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "text/calendar",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: `Task ${taskId} updated successfully`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
} catch (error: any) {
|
||||||
|
throw new Error(`Failed to update task: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== CALENDAR METHODS ==========
|
||||||
|
private async getCalendarEvents(args: any) {
|
||||||
|
const startDate = args.startDate || format(new Date(), "yyyy-MM-dd");
|
||||||
|
const endDate =
|
||||||
|
args.endDate ||
|
||||||
|
format(
|
||||||
|
new Date(Date.now() + 30 * 24 * 60 * 60 * 1000),
|
||||||
|
"yyyy-MM-dd"
|
||||||
|
);
|
||||||
|
const limit = args.limit || 50;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const caldavPath = `/remote.php/dav/calendars/${this.config.username}/personal/`;
|
||||||
|
|
||||||
|
const requestBody = `<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<c:calendar-query xmlns:c="urn:ietf:params:xml:ns:caldav" xmlns:d="DAV:">
|
||||||
|
<d:prop>
|
||||||
|
<d:getetag />
|
||||||
|
<c:calendar-data />
|
||||||
|
</d:prop>
|
||||||
|
<c:filter>
|
||||||
|
<c:comp-filter name="VCALENDAR">
|
||||||
|
<c:comp-filter name="VEVENT">
|
||||||
|
<c:time-range start="${this.formatICalDate(
|
||||||
|
new Date(startDate)
|
||||||
|
)}" end="${this.formatICalDate(new Date(endDate))}"/>
|
||||||
|
</c:comp-filter>
|
||||||
|
</c:comp-filter>
|
||||||
|
</c:filter>
|
||||||
|
</c:calendar-query>`;
|
||||||
|
|
||||||
|
const response = await this.axiosInstance.request({
|
||||||
|
method: "REPORT",
|
||||||
|
url: caldavPath,
|
||||||
|
data: requestBody,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/xml",
|
||||||
|
Depth: "1",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const events = this.parseEventsFromCalDAV(response.data, limit);
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: JSON.stringify(events, null, 2),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
} catch (error: any) {
|
||||||
|
throw new Error(`Failed to fetch calendar events: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseEventsFromCalDAV(xmlData: string, limit: number): any[] {
|
||||||
|
const events: any[] = [];
|
||||||
|
|
||||||
|
const eventMatches = xmlData.matchAll(
|
||||||
|
/<c:calendar-data[^>]*>([\s\S]*?)<\/c:calendar-data>/g
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const match of eventMatches) {
|
||||||
|
if (events.length >= limit) break;
|
||||||
|
|
||||||
|
const eventData = match[1];
|
||||||
|
const event = this.parseVEVENT(eventData);
|
||||||
|
|
||||||
|
if (event) {
|
||||||
|
events.push(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return events;
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseVEVENT(eventData: string): any | null {
|
||||||
|
const lines = eventData.split(/\r?\n/);
|
||||||
|
const event: any = {};
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
if (line.startsWith("UID:")) {
|
||||||
|
event.uid = line.substring(4).trim();
|
||||||
|
} else if (line.startsWith("SUMMARY:")) {
|
||||||
|
event.summary = line.substring(8).trim();
|
||||||
|
} else if (line.startsWith("DESCRIPTION:")) {
|
||||||
|
event.description = line.substring(12).trim();
|
||||||
|
} else if (line.startsWith("LOCATION:")) {
|
||||||
|
event.location = line.substring(9).trim();
|
||||||
|
} else if (line.startsWith("DTSTART")) {
|
||||||
|
const startMatch = line.match(/DTSTART[^:]*:(\d{8}T?\d{6}Z?)/);
|
||||||
|
if (startMatch) {
|
||||||
|
event.start = this.parseICalDate(startMatch[1]);
|
||||||
|
}
|
||||||
|
} else if (line.startsWith("DTEND")) {
|
||||||
|
const endMatch = line.match(/DTEND[^:]*:(\d{8}T?\d{6}Z?)/);
|
||||||
|
if (endMatch) {
|
||||||
|
event.end = this.parseICalDate(endMatch[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return event.uid ? event : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async createCalendarEvent(args: any) {
|
||||||
|
const { summary, description, startDateTime, endDateTime, location } = args;
|
||||||
|
const uid = this.generateUID();
|
||||||
|
|
||||||
|
let vevent = `BEGIN:VCALENDAR
|
||||||
|
VERSION:2.0
|
||||||
|
PRODID:-//Nextcloud MCP Server//EN
|
||||||
|
BEGIN:VEVENT
|
||||||
|
UID:${uid}
|
||||||
|
SUMMARY:${summary}
|
||||||
|
DTSTART:${this.formatICalDateTime(new Date(startDateTime))}
|
||||||
|
DTEND:${this.formatICalDateTime(new Date(endDateTime))}
|
||||||
|
CREATED:${this.formatICalDateTime(new Date())}`;
|
||||||
|
|
||||||
|
if (description) {
|
||||||
|
vevent += `\nDESCRIPTION:${description}`;
|
||||||
|
}
|
||||||
|
if (location) {
|
||||||
|
vevent += `\nLOCATION:${location}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
vevent += `\nEND:VEVENT
|
||||||
|
END:VCALENDAR`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const caldavPath = `/remote.php/dav/calendars/${this.config.username}/personal/${uid}.ics`;
|
||||||
|
|
||||||
|
await this.axiosInstance.put(caldavPath, vevent, {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "text/calendar",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: `Calendar event created successfully with UID: ${uid}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
} catch (error: any) {
|
||||||
|
throw new Error(`Failed to create calendar event: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== NOTES METHODS ==========
|
||||||
|
private async getNotes(args: any) {
|
||||||
|
const limit = args.limit || 50;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await this.axiosInstance.get(
|
||||||
|
`/index.php/apps/notes/api/v1/notes`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Accept: "application/json",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const notes = response.data.slice(0, limit);
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: JSON.stringify(notes, null, 2),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
} catch (error: any) {
|
||||||
|
throw new Error(`Failed to fetch notes: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async createNote(args: any) {
|
||||||
|
const { title, content, category } = args;
|
||||||
|
|
||||||
|
// Nextcloud Notes uses the first line as title
|
||||||
|
const noteContent = title ? `${title}\n\n${content}` : content;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const payload: any = {
|
||||||
|
content: noteContent,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (category) {
|
||||||
|
payload.category = category;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await this.axiosInstance.post(
|
||||||
|
`/index.php/apps/notes/api/v1/notes`,
|
||||||
|
payload,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Accept: "application/json",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: `Note created successfully with ID: ${response.data.id}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
} catch (error: any) {
|
||||||
|
throw new Error(`Failed to create note: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getNoteContent(args: any) {
|
||||||
|
const { noteId } = args;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await this.axiosInstance.get(
|
||||||
|
`/index.php/apps/notes/api/v1/notes/${noteId}`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Accept: "application/json",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: JSON.stringify(response.data, null, 2),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
} catch (error: any) {
|
||||||
|
throw new Error(`Failed to fetch note: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== EMAIL METHODS ==========
|
||||||
|
private async getEmails(args: any) {
|
||||||
|
const accountId = args.accountId || 0;
|
||||||
|
const limit = args.limit || 20;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get mailboxes first
|
||||||
|
const mailboxesResponse = await this.axiosInstance.get(
|
||||||
|
`/index.php/apps/mail/api/accounts/${accountId}/mailboxes`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Accept: "application/json",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Find INBOX
|
||||||
|
const inbox = mailboxesResponse.data.find(
|
||||||
|
(mb: any) => mb.specialRole === "inbox"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!inbox) {
|
||||||
|
throw new Error("Inbox not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get messages from inbox
|
||||||
|
const messagesResponse = await this.axiosInstance.get(
|
||||||
|
`/index.php/apps/mail/api/messages?mailboxId=${inbox.id}`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Accept: "application/json",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const emails = messagesResponse.data.slice(0, limit);
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: JSON.stringify(emails, null, 2),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
} catch (error: any) {
|
||||||
|
throw new Error(`Failed to fetch emails: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== UTILITY METHODS ==========
|
||||||
|
private generateUID(): string {
|
||||||
|
return `${Date.now()}-${Math.random().toString(36).substring(7)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private formatICalDate(date: Date): string {
|
||||||
|
return format(date, "yyyyMMdd");
|
||||||
|
}
|
||||||
|
|
||||||
|
private formatICalDateTime(date: Date): string {
|
||||||
|
return format(date, "yyyyMMdd'T'HHmmss'Z'");
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseICalDate(icalDate: string): string {
|
||||||
|
// Parse iCal date format (e.g., 20240101 or 20240101T120000Z)
|
||||||
|
if (icalDate.includes("T")) {
|
||||||
|
const year = icalDate.substring(0, 4);
|
||||||
|
const month = icalDate.substring(4, 6);
|
||||||
|
const day = icalDate.substring(6, 8);
|
||||||
|
const hour = icalDate.substring(9, 11);
|
||||||
|
const minute = icalDate.substring(11, 13);
|
||||||
|
return `${year}-${month}-${day} ${hour}:${minute}`;
|
||||||
|
} else {
|
||||||
|
const year = icalDate.substring(0, 4);
|
||||||
|
const month = icalDate.substring(4, 6);
|
||||||
|
const day = icalDate.substring(6, 8);
|
||||||
|
return `${year}-${month}-${day}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async run(): Promise<void> {
|
||||||
|
const transport = new StdioServerTransport();
|
||||||
|
await this.server.connect(transport);
|
||||||
|
console.error("Nextcloud MCP Server running on stdio");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main execution
|
||||||
|
const config: NextcloudConfig = {
|
||||||
|
url: process.env.NEXTCLOUD_URL || "",
|
||||||
|
username: process.env.NEXTCLOUD_USERNAME || "",
|
||||||
|
password: process.env.NEXTCLOUD_PASSWORD || "",
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!config.url || !config.username || !config.password) {
|
||||||
|
console.error(
|
||||||
|
"Error: NEXTCLOUD_URL, NEXTCLOUD_USERNAME, and NEXTCLOUD_PASSWORD environment variables are required"
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const server = new NextcloudMCPServer(config);
|
||||||
|
server.run().catch(console.error);
|
||||||
130
test-connection.js
Normal file
130
test-connection.js
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test script to verify Nextcloud connectivity
|
||||||
|
* Run with: node test-connection.js
|
||||||
|
*/
|
||||||
|
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
url: process.env.NEXTCLOUD_URL || '',
|
||||||
|
username: process.env.NEXTCLOUD_USERNAME || '',
|
||||||
|
password: process.env.NEXTCLOUD_PASSWORD || '',
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('🔍 Testing Nextcloud Connection...\n');
|
||||||
|
console.log('Configuration:');
|
||||||
|
console.log(` URL: ${config.url}`);
|
||||||
|
console.log(` Username: ${config.username}`);
|
||||||
|
console.log(` Password: ${config.password ? '***' + config.password.slice(-4) : '(not set)'}\n`);
|
||||||
|
|
||||||
|
if (!config.url || !config.username || !config.password) {
|
||||||
|
console.error('❌ Error: Missing configuration!');
|
||||||
|
console.error('Please set NEXTCLOUD_URL, NEXTCLOUD_USERNAME, and NEXTCLOUD_PASSWORD environment variables.');
|
||||||
|
console.error('\nYou can create a .env file with these values or export them:');
|
||||||
|
console.error(' export NEXTCLOUD_URL=https://your-nextcloud.com');
|
||||||
|
console.error(' export NEXTCLOUD_USERNAME=your-username');
|
||||||
|
console.error(' export NEXTCLOUD_PASSWORD=your-app-password\n');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const axiosInstance = axios.create({
|
||||||
|
baseURL: config.url,
|
||||||
|
auth: {
|
||||||
|
username: config.username,
|
||||||
|
password: config.password,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
async function testConnection() {
|
||||||
|
const tests = [
|
||||||
|
{
|
||||||
|
name: 'Basic Authentication',
|
||||||
|
test: async () => {
|
||||||
|
const response = await axiosInstance.get('/ocs/v2.php/cloud/user');
|
||||||
|
return { success: true, data: response.status === 200 };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'CalDAV (Tasks)',
|
||||||
|
test: async () => {
|
||||||
|
const response = await axiosInstance.get(
|
||||||
|
`/remote.php/dav/calendars/${config.username}/`
|
||||||
|
);
|
||||||
|
return { success: true, data: response.status === 207 || response.status === 200 };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Notes API',
|
||||||
|
test: async () => {
|
||||||
|
const response = await axiosInstance.get(
|
||||||
|
'/index.php/apps/notes/api/v1/notes',
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return { success: true, data: Array.isArray(response.data) };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Mail API (optional)',
|
||||||
|
test: async () => {
|
||||||
|
try {
|
||||||
|
const response = await axiosInstance.get(
|
||||||
|
'/index.php/apps/mail/api/accounts',
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return { success: true, data: Array.isArray(response.data) };
|
||||||
|
} catch (error) {
|
||||||
|
// Mail app might not be installed
|
||||||
|
return { success: false, data: 'Mail app not installed or not configured' };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
console.log('Running connectivity tests...\n');
|
||||||
|
|
||||||
|
for (const { name, test } of tests) {
|
||||||
|
process.stdout.write(`Testing ${name}... `);
|
||||||
|
try {
|
||||||
|
const result = await test();
|
||||||
|
if (result.success && result.data) {
|
||||||
|
console.log('✅ PASSED');
|
||||||
|
} else {
|
||||||
|
console.log(`⚠️ WARNING: ${result.data}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log('❌ FAILED');
|
||||||
|
if (axios.isAxiosError(error)) {
|
||||||
|
console.log(` Error: ${error.message}`);
|
||||||
|
if (error.response) {
|
||||||
|
console.log(` Status: ${error.response.status}`);
|
||||||
|
console.log(` Details: ${error.response.statusText}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log(` Error: ${error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('\n🎉 Connection test complete!\n');
|
||||||
|
console.log('Next steps:');
|
||||||
|
console.log('1. If all tests passed, you can use "npm run start" to run the MCP server');
|
||||||
|
console.log('2. If tests failed, check your credentials and Nextcloud configuration');
|
||||||
|
console.log('3. Make sure the required apps (Tasks, Calendar, Notes) are installed in Nextcloud\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
testConnection().catch((error) => {
|
||||||
|
console.error('\n❌ Fatal error:', error.message);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
17
tsconfig.json
Normal file
17
tsconfig.json
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "Node16",
|
||||||
|
"moduleResolution": "Node16",
|
||||||
|
"outDir": "./build",
|
||||||
|
"rootDir": "./src",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"declaration": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user