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
|
||||
MCP server for nextcloud
|
||||
# Nextcloud MCP Server
|
||||
|
||||
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