Initial commit: TickTick MCP Server with interactive setup
This commit is contained in:
commit
748bf79ab4
40 changed files with 10855 additions and 0 deletions
22
.env.example
Normal file
22
.env.example
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
# TickTick MCP Server Environment Variables
|
||||
# Copy this file to .env and fill in your actual values
|
||||
|
||||
# Required: Get these from https://developer.ticktick.com/
|
||||
TICKTICK_CLIENT_ID=your_client_id_here
|
||||
TICKTICK_CLIENT_SECRET=your_client_secret_here
|
||||
|
||||
# Optional: OAuth redirect URI (defaults to http://localhost:3000/callback)
|
||||
TICKTICK_REDIRECT_URI=http://localhost:3000/callback
|
||||
|
||||
# Optional: Pre-authenticated tokens (get these after OAuth flow)
|
||||
TICKTICK_ACCESS_TOKEN=your_access_token_here
|
||||
TICKTICK_REFRESH_TOKEN=your_refresh_token_here
|
||||
|
||||
# How to get credentials:
|
||||
# 1. Visit https://developer.ticktick.com/
|
||||
# 2. Login with your TickTick account
|
||||
# 3. Click "Manage Apps" in the top right
|
||||
# 4. Click "+App Name" to create a new app
|
||||
# 5. Enter any app name (e.g., "My MCP Server")
|
||||
# 6. Copy the generated Client ID and Client Secret
|
||||
# 7. Set OAuth Redirect URL to: http://localhost:3000/callback
|
||||
106
.gitignore
vendored
Normal file
106
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
# Dependencies
|
||||
node_modules/
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Build output
|
||||
dist/
|
||||
build/
|
||||
*.tsbuildinfo
|
||||
|
||||
# Environment variables
|
||||
.env
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# IDE files
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS generated files
|
||||
.DS_Store
|
||||
.DS_Store?
|
||||
._*
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
ehthumbs.db
|
||||
Thumbs.db
|
||||
|
||||
# Logs
|
||||
logs/
|
||||
*.log
|
||||
|
||||
# Runtime data
|
||||
pids/
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage/
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Dependency directories
|
||||
jspm_packages/
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Microbundle cache
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
.parcel-cache
|
||||
|
||||
# next.js build output
|
||||
.next
|
||||
|
||||
# nuxt.js build output
|
||||
.nuxt
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
# TernJS port file
|
||||
.tern-port
|
||||
|
||||
# Stores VSCode versions used for testing VSCode extensions
|
||||
.vscode-test
|
||||
|
||||
# Temporary folders
|
||||
tmp/
|
||||
temp/
|
||||
129
AUTHENTICATION.md
Normal file
129
AUTHENTICATION.md
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
# TickTick MCP Server Authentication Guide
|
||||
|
||||
## 🔑 Authentication Options
|
||||
|
||||
### Option 1: Demo Credentials (Quick Start)
|
||||
|
||||
For testing and evaluation purposes, you can use our shared demo credentials:
|
||||
|
||||
```bash
|
||||
# Demo credentials (read-only access to demo account)
|
||||
export TICKTICK_CLIENT_ID="rbCnP4Mk9YgDdpPR86"
|
||||
export TICKTICK_CLIENT_SECRET="*0zQ(kyNSzVmi#jBX@D4BKn%r3*9^99G"
|
||||
export TICKTICK_REDIRECT_URI="http://localhost:3000/api/ticktick/callback"
|
||||
export TICKTICK_ACCESS_TOKEN="demo_access_token_here"
|
||||
export TICKTICK_REFRESH_TOKEN="demo_refresh_token_here"
|
||||
```
|
||||
|
||||
**Demo Account Limitations:**
|
||||
- ✅ Full MCP functionality testing
|
||||
- ✅ Tools, Resources, Prompts testing
|
||||
- ⚠️ Shared with other users
|
||||
- ⚠️ Data may be reset periodically
|
||||
- ❌ Not suitable for production use
|
||||
|
||||
### Option 2: Your Own TickTick API Credentials (Production)
|
||||
|
||||
For production use with your personal TickTick data:
|
||||
|
||||
#### Step 1: Create TickTick Developer App
|
||||
1. Visit [TickTick Developer Portal](https://developer.ticktick.com/)
|
||||
2. Login with your TickTick account
|
||||
3. Click "Manage Apps" → "+App Name"
|
||||
4. Enter app name (e.g., "My Personal MCP Server")
|
||||
5. Set redirect URI to: `http://localhost:3000/callback`
|
||||
6. Copy your Client ID and Client Secret
|
||||
|
||||
#### Step 2: Configure Environment
|
||||
```bash
|
||||
# Your personal credentials
|
||||
export TICKTICK_CLIENT_ID="your_client_id"
|
||||
export TICKTICK_CLIENT_SECRET="your_client_secret"
|
||||
export TICKTICK_REDIRECT_URI="http://localhost:3000/callback"
|
||||
|
||||
# Run OAuth flow to get access tokens
|
||||
npm run test-oauth
|
||||
```
|
||||
|
||||
### Option 3: No-Auth Mode (Limited Functionality)
|
||||
|
||||
Run in demo mode without TickTick credentials:
|
||||
|
||||
```bash
|
||||
# No authentication - returns mock data
|
||||
export TICKTICK_DEMO_MODE="true"
|
||||
```
|
||||
|
||||
**Demo Mode Features:**
|
||||
- ✅ MCP protocol testing
|
||||
- ✅ Tool interface validation
|
||||
- ✅ Mock data responses
|
||||
- ❌ No real TickTick integration
|
||||
- ❌ No actual task management
|
||||
|
||||
## 🚀 Quick Setup Commands
|
||||
|
||||
### Demo Mode (Fastest)
|
||||
```bash
|
||||
npm install @ticktick-ecosystem/mcp-server
|
||||
npx ticktick-mcp-server --demo
|
||||
```
|
||||
|
||||
### Production Mode
|
||||
```bash
|
||||
npm install @ticktick-ecosystem/mcp-server
|
||||
npm run setup-env # Interactive setup
|
||||
npm run test-oauth # Get access tokens
|
||||
npx ticktick-mcp-server
|
||||
```
|
||||
|
||||
## 🔐 Security Considerations
|
||||
|
||||
### For Demo Credentials:
|
||||
- Only use for testing and evaluation
|
||||
- Demo account data is shared and temporary
|
||||
- No sensitive personal information
|
||||
|
||||
### For Personal Credentials:
|
||||
- Keep your Client Secret secure
|
||||
- Never share access tokens publicly
|
||||
- Use environment variables, not hardcoded values
|
||||
- Regularly rotate credentials if needed
|
||||
|
||||
### For Production Deployment:
|
||||
```bash
|
||||
# Secure environment variable setup
|
||||
echo "TICKTICK_CLIENT_ID=your_client_id" >> .env
|
||||
echo "TICKTICK_CLIENT_SECRET=your_client_secret" >> .env
|
||||
chmod 600 .env # Restrict file permissions
|
||||
```
|
||||
|
||||
## 🎯 Recommended Approach
|
||||
|
||||
1. **First Time Users**: Start with demo credentials to test functionality
|
||||
2. **Personal Use**: Set up your own TickTick app for real data access
|
||||
3. **Enterprise/Team**: Each user creates their own TickTick app
|
||||
|
||||
## 🆘 Troubleshooting
|
||||
|
||||
### Demo Credentials Not Working
|
||||
- Demo account may be temporarily unavailable
|
||||
- Try personal credentials setup
|
||||
- Check for API rate limits
|
||||
|
||||
### Personal Setup Issues
|
||||
- Verify TickTick Developer Portal access
|
||||
- Ensure correct redirect URI
|
||||
- Check OAuth flow completion
|
||||
|
||||
### Production Issues
|
||||
- Validate environment variables
|
||||
- Test token refresh mechanism
|
||||
- Monitor API rate limits
|
||||
|
||||
## 📞 Support
|
||||
|
||||
Need help with authentication setup?
|
||||
- 📖 [Main README](README.md)
|
||||
- 🧪 [Testing Guide](TESTING.md)
|
||||
- 🐛 [Report Issues](https://github.com/your-username/ticktick-mcp-server/issues)
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2024 TickTick Ecosystem
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
333
README.md
Normal file
333
README.md
Normal file
|
|
@ -0,0 +1,333 @@
|
|||
# @ticktick-ecosystem/mcp-server
|
||||
|
||||
A Model Context Protocol (MCP) server for integrating TickTick task management with AI applications like Claude, ChatGPT, and other LLM-powered tools.
|
||||
|
||||
[](https://badge.fury.io/js/%40ticktick-ecosystem%2Fmcp-server)
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
[](https://nodejs.org/)
|
||||
|
||||
## ✨ Features
|
||||
|
||||
### 🛠️ Tools (AI Actions)
|
||||
- **Task Management**
|
||||
- `create_task` - Create new tasks with due dates, priorities, and projects
|
||||
- `get_tasks` - Retrieve tasks with filtering options
|
||||
- `update_task` - Modify existing tasks
|
||||
- `complete_task` - Mark tasks as completed
|
||||
- `delete_task` - Remove tasks
|
||||
- `search_tasks` - Search tasks by title or content
|
||||
- `get_today_tasks` - Get today's scheduled tasks
|
||||
- `get_overdue_tasks` - Get overdue tasks
|
||||
|
||||
- **Project Management**
|
||||
- `get_projects` - List all projects
|
||||
- `create_project` - Create new projects
|
||||
- `update_project` - Modify project details
|
||||
- `delete_project` - Remove projects
|
||||
- `get_project_tasks` - Get tasks within a specific project
|
||||
|
||||
### 📊 Resources (Data Access)
|
||||
- `ticktick://tasks/today` - Today's tasks
|
||||
- `ticktick://tasks/overdue` - Overdue tasks
|
||||
- `ticktick://tasks/completed` - Recently completed tasks
|
||||
- `ticktick://projects/all` - All projects
|
||||
- `ticktick://stats/summary` - Productivity statistics
|
||||
|
||||
### 💡 Prompts (AI Assistance)
|
||||
- `daily_planning` - AI-powered daily task planning
|
||||
- `task_breakdown` - Break complex tasks into subtasks
|
||||
- `priority_analysis` - Analyze and suggest task priorities
|
||||
- `weekly_review` - Review productivity and plan ahead
|
||||
- `project_planning` - Comprehensive project planning
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
### Demo Mode (No Authentication Required)
|
||||
|
||||
Perfect for testing and evaluation:
|
||||
|
||||
```bash
|
||||
# Install and run in demo mode
|
||||
npm install -g @ticktick-ecosystem/mcp-server
|
||||
ticktick-mcp-server --demo
|
||||
```
|
||||
|
||||
Or with npx:
|
||||
```bash
|
||||
npx @ticktick-ecosystem/mcp-server --demo
|
||||
```
|
||||
|
||||
### Claude Desktop Integration (Demo Mode)
|
||||
|
||||
Add to your Claude Desktop configuration file:
|
||||
|
||||
**macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
|
||||
**Windows**: `%APPDATA%\Claude\claude_desktop_config.json`
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"ticktick": {
|
||||
"command": "npx",
|
||||
"args": ["@ticktick-ecosystem/mcp-server", "--demo"],
|
||||
"env": {
|
||||
"TICKTICK_DEMO_MODE": "true"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Restart Claude Desktop and start asking about your tasks!
|
||||
|
||||
## 📦 Installation
|
||||
|
||||
```bash
|
||||
npm install @ticktick-ecosystem/mcp-server
|
||||
```
|
||||
|
||||
Or install globally:
|
||||
```bash
|
||||
npm install -g @ticktick-ecosystem/mcp-server
|
||||
```
|
||||
|
||||
## 🔧 Authentication Setup (For Real Data)
|
||||
|
||||
### 1. Get TickTick API Credentials
|
||||
|
||||
1. Visit [TickTick Developer Portal](https://developer.ticktick.com/)
|
||||
2. Create a new application
|
||||
3. Note your Client ID and Client Secret
|
||||
4. Set OAuth Redirect URL to: `http://localhost:3000/callback`
|
||||
|
||||
### 2. Environment Configuration
|
||||
|
||||
Create a `.env` file or set environment variables:
|
||||
|
||||
```bash
|
||||
# Required
|
||||
TICKTICK_CLIENT_ID=your_client_id_here
|
||||
TICKTICK_CLIENT_SECRET=your_client_secret_here
|
||||
|
||||
# Optional (will be obtained through OAuth if not provided)
|
||||
TICKTICK_REDIRECT_URI=http://localhost:3000/callback
|
||||
TICKTICK_ACCESS_TOKEN=your_access_token_here
|
||||
TICKTICK_REFRESH_TOKEN=your_refresh_token_here
|
||||
```
|
||||
|
||||
### 3. Interactive Setup (Recommended)
|
||||
|
||||
```bash
|
||||
# Install the package
|
||||
npm install -g @ticktick-ecosystem/mcp-server
|
||||
|
||||
# Run interactive setup
|
||||
npx @ticktick-ecosystem/mcp-server --setup
|
||||
```
|
||||
|
||||
This will guide you through:
|
||||
1. Getting TickTick API credentials
|
||||
2. OAuth authorization flow
|
||||
3. Automatic configuration saving to `~/.ticktick-mcp/config.json`
|
||||
|
||||
## 🎯 Usage Examples
|
||||
|
||||
### Creating Tasks with AI
|
||||
|
||||
```
|
||||
User: "Create a task to review the quarterly budget report, due next Friday with high priority"
|
||||
|
||||
AI Response: "I'll create that task for you with high priority and set the due date for next Friday."
|
||||
|
||||
Result: New task created in TickTick with proper priority and due date
|
||||
```
|
||||
|
||||
### Daily Planning
|
||||
|
||||
```
|
||||
User: "Help me plan my day"
|
||||
|
||||
AI Response: Based on your current tasks, I recommend:
|
||||
1. Morning: Focus on the quarterly budget review (high priority)
|
||||
2. Afternoon: Team meeting preparation
|
||||
3. Evening: Code review for the new feature
|
||||
|
||||
Would you like me to adjust any task priorities or deadlines?
|
||||
```
|
||||
|
||||
### Project Management
|
||||
|
||||
```
|
||||
User: "Show me all tasks in my 'Website Redesign' project that are overdue"
|
||||
|
||||
AI Response: [Lists overdue tasks with details and suggests next actions]
|
||||
```
|
||||
|
||||
## 🔧 Configuration
|
||||
|
||||
### Demo Mode Features
|
||||
- ✅ No authentication required
|
||||
- ✅ Uses realistic mock data
|
||||
- ✅ All functions work with sample tasks and projects
|
||||
- ✅ Perfect for testing and evaluation
|
||||
- ✅ Safe for public demonstrations
|
||||
|
||||
### Production Mode Features
|
||||
- 🔄 Real-time TickTick synchronization
|
||||
- 🔐 OAuth 2.0 secure authentication
|
||||
- 📊 Access to your actual tasks and projects
|
||||
- 🔄 Automatic token refresh
|
||||
- 📈 Real productivity statistics
|
||||
|
||||
### Claude Desktop Configuration (Production)
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"ticktick": {
|
||||
"command": "npx",
|
||||
"args": ["@ticktick-ecosystem/mcp-server"],
|
||||
"env": {
|
||||
"TICKTICK_CLIENT_ID": "your_client_id",
|
||||
"TICKTICK_CLIENT_SECRET": "your_client_secret",
|
||||
"TICKTICK_ACCESS_TOKEN": "your_access_token",
|
||||
"TICKTICK_REFRESH_TOKEN": "your_refresh_token"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Other MCP Clients
|
||||
|
||||
The server uses stdio transport and is compatible with any MCP-compliant client:
|
||||
|
||||
```bash
|
||||
# For MCP Inspector
|
||||
npx @modelcontextprotocol/inspector npx @ticktick-ecosystem/mcp-server --demo
|
||||
|
||||
# For custom integrations
|
||||
npx @ticktick-ecosystem/mcp-server
|
||||
```
|
||||
|
||||
## 📊 API Reference
|
||||
|
||||
### Task Priority Levels
|
||||
- `0` - None
|
||||
- `1` - Low
|
||||
- `3` - Medium
|
||||
- `5` - High
|
||||
|
||||
### Date Formats
|
||||
- Due dates: ISO format (`YYYY-MM-DD` or `YYYY-MM-DDTHH:mm:ss`)
|
||||
- All dates are in UTC unless timezone is specified
|
||||
|
||||
### Response Format
|
||||
All tools return structured responses:
|
||||
|
||||
**Success:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": { ... },
|
||||
"message": "Operation completed successfully"
|
||||
}
|
||||
```
|
||||
|
||||
**Error:**
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": "Error description"
|
||||
}
|
||||
```
|
||||
|
||||
## 🧪 Testing
|
||||
|
||||
```bash
|
||||
# Quick demo test
|
||||
npm run demo
|
||||
|
||||
# With MCP Inspector
|
||||
npm install -g @modelcontextprotocol/inspector
|
||||
npx @modelcontextprotocol/inspector npx @ticktick-ecosystem/mcp-server --demo
|
||||
```
|
||||
|
||||
See [TESTING.md](TESTING.md) for comprehensive testing instructions.
|
||||
|
||||
## 🔒 Security & Privacy
|
||||
|
||||
- OAuth 2.0 secure authentication
|
||||
- Tokens stored locally only
|
||||
- No data collection or telemetry
|
||||
- Open source and auditable
|
||||
- Demo mode uses no real data
|
||||
|
||||
## 🛠️ Development
|
||||
|
||||
```bash
|
||||
# Clone repository
|
||||
git clone https://github.com/ticktick-ecosystem/mcp-server
|
||||
cd mcp-server
|
||||
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
# Build
|
||||
npm run build
|
||||
|
||||
# Development mode
|
||||
npm run dev
|
||||
|
||||
# Run tests
|
||||
npm test
|
||||
|
||||
# Demo mode
|
||||
npm run demo
|
||||
```
|
||||
|
||||
## 📖 Documentation
|
||||
|
||||
- [Authentication Guide](AUTHENTICATION.md) - Detailed authentication setup
|
||||
- [Testing Guide](TESTING.md) - Comprehensive testing instructions
|
||||
- [MCP Protocol](https://modelcontextprotocol.io/) - Learn about Model Context Protocol
|
||||
- [TickTick API](https://developer.ticktick.com/) - Official TickTick API documentation
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
We welcome contributions! Please see our contributing guidelines:
|
||||
|
||||
1. Fork the repository
|
||||
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
|
||||
3. Commit your changes (`git commit -m 'Add amazing feature'`)
|
||||
4. Push to the branch (`git push origin feature/amazing-feature`)
|
||||
5. Open a Pull Request
|
||||
|
||||
## 📄 License
|
||||
|
||||
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
||||
|
||||
## 🆘 Support
|
||||
|
||||
- 📖 [Documentation](https://github.com/ticktick-ecosystem/mcp-server#readme)
|
||||
- 🐛 [Issue Tracker](https://github.com/ticktick-ecosystem/mcp-server/issues)
|
||||
- 💬 [Discussions](https://github.com/ticktick-ecosystem/mcp-server/discussions)
|
||||
|
||||
## 🌟 Related Projects
|
||||
|
||||
- [Model Context Protocol](https://modelcontextprotocol.io/) - The protocol this server implements
|
||||
- [TickTick](https://ticktick.com/) - The task management platform
|
||||
- [Claude Desktop](https://claude.ai/desktop) - AI assistant with MCP support
|
||||
- [MCP Inspector](https://github.com/modelcontextprotocol/inspector) - Tool for testing MCP servers
|
||||
|
||||
## 🙏 Acknowledgments
|
||||
|
||||
- [Anthropic](https://anthropic.com/) for developing the Model Context Protocol
|
||||
- [TickTick](https://ticktick.com/) for providing the task management API
|
||||
- The MCP community for feedback and contributions
|
||||
|
||||
---
|
||||
|
||||
**Made with ❤️ for the TickTick and MCP communities**
|
||||
|
||||
🎉 **Ready to supercharge your productivity with AI-powered task management!**
|
||||
252
TESTING.md
Normal file
252
TESTING.md
Normal file
|
|
@ -0,0 +1,252 @@
|
|||
# Testing TickTick MCP Server
|
||||
|
||||
This guide covers how to test the TickTick MCP Server with various MCP clients.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. Build the server:
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
2. For testing with real TickTick data, set up your environment:
|
||||
```bash
|
||||
cp .env.example .env
|
||||
# Edit .env with your TickTick credentials
|
||||
```
|
||||
|
||||
## Quick Demo Test
|
||||
|
||||
A simple test script is included to verify functionality:
|
||||
|
||||
```bash
|
||||
# Build and run quick demo test
|
||||
npm run build
|
||||
node test-demo.js
|
||||
```
|
||||
|
||||
## Testing with MCP Inspector
|
||||
|
||||
MCP Inspector is the official testing tool for MCP servers.
|
||||
|
||||
### Install MCP Inspector
|
||||
```bash
|
||||
npm install -g @modelcontextprotocol/inspector
|
||||
```
|
||||
|
||||
### Test Demo Mode (Recommended)
|
||||
```bash
|
||||
# Start server in demo mode - no authentication needed
|
||||
TICKTICK_DEMO_MODE=true npx @modelcontextprotocol/inspector node dist/index.js --demo
|
||||
```
|
||||
|
||||
### Test with Real Credentials
|
||||
```bash
|
||||
# Make sure .env is configured, then:
|
||||
npx @modelcontextprotocol/inspector node dist/index.js
|
||||
```
|
||||
|
||||
## Testing with Claude Desktop
|
||||
|
||||
### Configuration
|
||||
|
||||
Add this to your Claude Desktop configuration file:
|
||||
|
||||
**macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
|
||||
**Windows**: `%APPDATA%\Claude\claude_desktop_config.json`
|
||||
|
||||
#### Demo Mode Configuration (Recommended for Testing)
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"ticktick": {
|
||||
"command": "node",
|
||||
"args": ["/Users/takashishibata/Desktop/creative-lab/mcp-research/ticktick-mcp-server/dist/index.js", "--demo"],
|
||||
"env": {
|
||||
"TICKTICK_DEMO_MODE": "true"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Production Configuration
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"ticktick": {
|
||||
"command": "node",
|
||||
"args": ["/path/to/ticktick-mcp-server/dist/index.js"],
|
||||
"env": {
|
||||
"TICKTICK_CLIENT_ID": "your_client_id",
|
||||
"TICKTICK_CLIENT_SECRET": "your_client_secret",
|
||||
"TICKTICK_ACCESS_TOKEN": "your_access_token",
|
||||
"TICKTICK_REFRESH_TOKEN": "your_refresh_token"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Note**: A sample configuration file is provided at `claude-desktop-config.json` for reference.
|
||||
|
||||
### Restart Claude Desktop
|
||||
|
||||
After updating the configuration, restart Claude Desktop to load the new MCP server.
|
||||
|
||||
## Available Functions
|
||||
|
||||
### Task Management
|
||||
- `create_task` - Create a new task
|
||||
- `get_tasks` - Get all tasks with filters
|
||||
- `update_task` - Update an existing task
|
||||
- `complete_task` - Mark a task as completed
|
||||
- `delete_task` - Delete a task
|
||||
- `search_tasks` - Search tasks by title/content
|
||||
- `get_today_tasks` - Get today's tasks
|
||||
- `get_overdue_tasks` - Get overdue tasks
|
||||
|
||||
### Project Management
|
||||
- `create_project` - Create a new project
|
||||
- `get_projects` - Get all projects
|
||||
- `update_project` - Update a project
|
||||
- `delete_project` - Delete a project
|
||||
- `get_project_tasks` - Get tasks in a specific project
|
||||
|
||||
### Resources
|
||||
- `ticktick://tasks/today` - Today's tasks
|
||||
- `ticktick://tasks/overdue` - Overdue tasks
|
||||
- `ticktick://tasks/completed` - Recently completed tasks
|
||||
- `ticktick://projects/all` - All projects
|
||||
- `ticktick://stats/summary` - Productivity statistics
|
||||
|
||||
### Prompts
|
||||
- `daily_planning` - Plan your day with current tasks
|
||||
- `task_breakdown` - Break down complex tasks
|
||||
- `priority_analysis` - Analyze task priorities
|
||||
- `weekly_review` - Review weekly progress
|
||||
- `project_planning` - Plan project milestones
|
||||
|
||||
## Demo Mode Features
|
||||
|
||||
When running in demo mode (`--demo` or `TICKTICK_DEMO_MODE=true`):
|
||||
|
||||
- ✅ Uses mock data instead of real TickTick API
|
||||
- ✅ No authentication required
|
||||
- ✅ Safe for testing and demonstrations
|
||||
- ✅ Includes realistic sample tasks and projects
|
||||
- ✅ All functions work with mock data
|
||||
- ✅ Perfect for NPM package evaluation
|
||||
|
||||
## Testing Examples
|
||||
|
||||
### Create a Task
|
||||
```
|
||||
Can you create a task called "Review MCP documentation" for tomorrow?
|
||||
```
|
||||
|
||||
### Get Today's Tasks
|
||||
```
|
||||
What tasks do I have scheduled for today?
|
||||
```
|
||||
|
||||
### Plan My Day
|
||||
```
|
||||
Can you help me plan my day using the daily_planning prompt?
|
||||
```
|
||||
|
||||
### Search Tasks
|
||||
```
|
||||
Find all tasks related to "documentation"
|
||||
```
|
||||
|
||||
### Check Resources
|
||||
```
|
||||
Show me my productivity statistics from the TickTick resource
|
||||
```
|
||||
|
||||
## OAuth Setup (For Real Data)
|
||||
|
||||
### 1. Get TickTick API Credentials
|
||||
|
||||
1. Visit [TickTick Developer Portal](https://developer.ticktick.com/)
|
||||
2. Login with your TickTick account
|
||||
3. Click "Manage Apps" in the top right
|
||||
4. Click "+App Name" to create a new app
|
||||
5. Enter any app name (e.g., "My MCP Server")
|
||||
6. Copy the generated Client ID and Client Secret
|
||||
7. Set OAuth Redirect URL to: `http://localhost:3000/callback`
|
||||
|
||||
### 2. Environment Setup
|
||||
```bash
|
||||
# Set up environment variables
|
||||
npm run setup-env
|
||||
# Follow the prompts to enter your TickTick API credentials
|
||||
|
||||
# Build the project
|
||||
npm run build
|
||||
```
|
||||
|
||||
### 3. OAuth Authentication
|
||||
```bash
|
||||
# Run OAuth helper to get access tokens
|
||||
npm run test-oauth
|
||||
```
|
||||
|
||||
This will:
|
||||
- Start a local server on port 3000
|
||||
- Open your browser to authorize the app
|
||||
- Exchange the authorization code for access tokens
|
||||
- Display the tokens to add to your .env file
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Server Won't Start
|
||||
- Check that Node.js version is 18 or higher
|
||||
- Verify the build completed successfully: `npm run build`
|
||||
- Check environment variables are set correctly
|
||||
|
||||
### Authentication Issues (Real Data Mode)
|
||||
- Verify your TickTick credentials in `.env`
|
||||
- Check that your TickTick app has the correct redirect URI
|
||||
- Try demo mode to isolate authentication issues: `--demo`
|
||||
|
||||
### Claude Desktop Integration
|
||||
- Verify the path to `dist/index.js` is correct in the configuration
|
||||
- Check Claude Desktop logs for connection errors
|
||||
- Restart Claude Desktop after configuration changes
|
||||
- Use the provided `claude-desktop-config.json` as a reference
|
||||
|
||||
### Connection Errors
|
||||
- Ensure the server executable has proper permissions
|
||||
- Check that all dependencies are installed: `npm install`
|
||||
- Verify the MCP SDK version compatibility
|
||||
- Try the simple test script: `node test-demo.js`
|
||||
|
||||
### Demo Mode Not Working
|
||||
- Ensure `TICKTICK_DEMO_MODE=true` is set in environment
|
||||
- Or use the `--demo` command line flag
|
||||
- Check that mock data is loading properly in console output
|
||||
|
||||
## API Rate Limits
|
||||
|
||||
TickTick API has rate limits:
|
||||
- Be mindful of request frequency
|
||||
- Implement proper error handling for rate limit responses
|
||||
- Consider caching for frequently accessed data
|
||||
|
||||
## Next Steps
|
||||
|
||||
After successful testing:
|
||||
1. ✅ All tools work correctly
|
||||
2. ✅ Resources return proper data
|
||||
3. ✅ Prompts generate helpful content
|
||||
4. ✅ Claude Desktop integration works
|
||||
5. ✅ Ready for NPM publication
|
||||
|
||||
## Support
|
||||
|
||||
- 📖 [Main README](README.md)
|
||||
- 🔛 [Authentication Guide](AUTHENTICATION.md)
|
||||
- 🐛 Issue Tracker (will be available after GitHub repository creation)
|
||||
- 💬 Discussions (will be available after GitHub repository creation)
|
||||
33
build-scripts/test-build.sh
Normal file
33
build-scripts/test-build.sh
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
#!/bin/bash
|
||||
cd /Users/takashishibata/Desktop/creative-lab/mcp-research/ticktick-mcp-server
|
||||
|
||||
echo "=== Starting TypeScript Build Test ==="
|
||||
echo "Current directory: $(pwd)"
|
||||
echo ""
|
||||
|
||||
# Check if node_modules exists
|
||||
if [ ! -d "node_modules" ]; then
|
||||
echo "⚠️ node_modules not found, installing dependencies..."
|
||||
npm install
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# Check TypeScript compiler
|
||||
echo "=== Checking TypeScript Installation ==="
|
||||
npx tsc --version
|
||||
echo ""
|
||||
|
||||
# Run TypeScript compilation with verbose output
|
||||
echo "=== Running TypeScript Compilation ==="
|
||||
npx tsc --noEmit --listFiles | head -20
|
||||
echo ""
|
||||
|
||||
echo "=== Checking for compilation errors ==="
|
||||
npx tsc --noEmit 2>&1
|
||||
|
||||
echo ""
|
||||
echo "=== Attempting full build ==="
|
||||
npm run build 2>&1
|
||||
|
||||
echo ""
|
||||
echo "=== Build Test Complete ==="
|
||||
84
check-tsc-errors.js
Normal file
84
check-tsc-errors.js
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
const { execSync } = require('child_process');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
console.log('TickTick MCP Server - TypeScript Error Check');
|
||||
console.log('============================================');
|
||||
|
||||
const projectDir = '/Users/takashishibata/Desktop/creative-lab/mcp-research/ticktick-mcp-server';
|
||||
|
||||
try {
|
||||
// Change to project directory
|
||||
process.chdir(projectDir);
|
||||
console.log(`Working in: ${process.cwd()}`);
|
||||
|
||||
// First, try compiling the test file
|
||||
console.log('\n1. Testing simple TypeScript compilation...');
|
||||
try {
|
||||
const output = execSync('npx tsc test-compile.ts --noEmit --target ES2022 --module ESNext --moduleResolution node', {
|
||||
encoding: 'utf8',
|
||||
stdio: 'pipe'
|
||||
});
|
||||
console.log('✅ Simple compilation successful');
|
||||
} catch (error) {
|
||||
console.log('❌ Simple compilation failed:');
|
||||
console.log('STDOUT:', error.stdout);
|
||||
console.log('STDERR:', error.stderr);
|
||||
}
|
||||
|
||||
// Now try the full project
|
||||
console.log('\n2. Testing full project compilation...');
|
||||
try {
|
||||
const output = execSync('npx tsc --noEmit', {
|
||||
encoding: 'utf8',
|
||||
stdio: 'pipe'
|
||||
});
|
||||
console.log('✅ Full project type check successful');
|
||||
} catch (error) {
|
||||
console.log('❌ Full project type check failed:');
|
||||
console.log('STDOUT:', error.stdout);
|
||||
console.log('STDERR:', error.stderr);
|
||||
|
||||
// Try to identify specific problems
|
||||
console.log('\n3. Analyzing errors...');
|
||||
const errorOutput = error.stderr || error.stdout || '';
|
||||
|
||||
if (errorOutput.includes('Cannot find module')) {
|
||||
console.log('🔍 Module resolution issues detected');
|
||||
}
|
||||
if (errorOutput.includes('has no exported member')) {
|
||||
console.log('🔍 Export/import issues detected');
|
||||
}
|
||||
if (errorOutput.includes('Type')) {
|
||||
console.log('🔍 Type definition issues detected');
|
||||
}
|
||||
}
|
||||
|
||||
// Try building
|
||||
console.log('\n4. Testing build...');
|
||||
try {
|
||||
const output = execSync('npx tsc', {
|
||||
encoding: 'utf8',
|
||||
stdio: 'pipe'
|
||||
});
|
||||
console.log('✅ Build successful');
|
||||
|
||||
// Check dist directory
|
||||
if (fs.existsSync('dist')) {
|
||||
const files = fs.readdirSync('dist');
|
||||
console.log('📁 Built files:', files.slice(0, 5));
|
||||
if (files.length > 5) {
|
||||
console.log(` ... and ${files.length - 5} more files`);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('❌ Build failed:');
|
||||
console.log('STDOUT:', error.stdout);
|
||||
console.log('STDERR:', error.stderr);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('💥 Unexpected error:', error.message);
|
||||
}
|
||||
|
||||
console.log('\n============================================');
|
||||
133
check-types-final.js
Normal file
133
check-types-final.js
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const { spawn } = require('child_process');
|
||||
const path = require('path');
|
||||
|
||||
console.log('🔍 TickTick MCP Server - Final TypeScript Check');
|
||||
console.log('==============================================\n');
|
||||
|
||||
// Change to project directory
|
||||
const projectDir = '/Users/takashishibata/Desktop/creative-lab/mcp-research/ticktick-mcp-server';
|
||||
process.chdir(projectDir);
|
||||
|
||||
console.log(`📂 Working directory: ${process.cwd()}\n`);
|
||||
|
||||
function runCommand(command, args = []) {
|
||||
return new Promise((resolve, reject) => {
|
||||
console.log(`⚡ Executing: ${command} ${args.join(' ')}`);
|
||||
|
||||
const child = spawn(command, args, {
|
||||
stdio: 'pipe',
|
||||
shell: true
|
||||
});
|
||||
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
|
||||
child.stdout.on('data', (data) => {
|
||||
stdout += data.toString();
|
||||
});
|
||||
|
||||
child.stderr.on('data', (data) => {
|
||||
stderr += data.toString();
|
||||
});
|
||||
|
||||
child.on('close', (code) => {
|
||||
resolve({ code, stdout, stderr });
|
||||
});
|
||||
|
||||
child.on('error', (error) => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
// Step 1: TypeScript Version Check
|
||||
console.log('📋 Step 1: TypeScript Version Check');
|
||||
console.log('-----------------------------------');
|
||||
const versionResult = await runCommand('npx', ['tsc', '--version']);
|
||||
console.log(`TypeScript Version: ${versionResult.stdout.trim()}`);
|
||||
console.log('✅ Version check complete\n');
|
||||
|
||||
// Step 2: TypeScript Type Check (no emit)
|
||||
console.log('🔍 Step 2: TypeScript Type Check (no emit)');
|
||||
console.log('-------------------------------------------');
|
||||
const typeCheckResult = await runCommand('npx', ['tsc', '--noEmit']);
|
||||
|
||||
if (typeCheckResult.code === 0) {
|
||||
console.log('✅ TypeScript type check: PASSED');
|
||||
console.log(' No type errors found!');
|
||||
} else {
|
||||
console.log('❌ TypeScript type check: FAILED');
|
||||
console.log(' Errors found:');
|
||||
console.log(typeCheckResult.stderr);
|
||||
|
||||
// Count and categorize errors
|
||||
const errorLines = typeCheckResult.stderr.split('\n').filter(line => line.includes('error TS'));
|
||||
console.log(`\n📊 Error Summary: ${errorLines.length} errors found`);
|
||||
}
|
||||
console.log('');
|
||||
|
||||
// Step 3: Full Build Test (only if type check passed)
|
||||
if (typeCheckResult.code === 0) {
|
||||
console.log('🏗️ Step 3: Full Build Test');
|
||||
console.log('---------------------------');
|
||||
const buildResult = await runCommand('npm', ['run', 'build']);
|
||||
|
||||
if (buildResult.code === 0) {
|
||||
console.log('✅ Build: SUCCESS');
|
||||
|
||||
// Check build output
|
||||
const fs = require('fs');
|
||||
try {
|
||||
const distExists = fs.existsSync('./dist');
|
||||
const indexExists = fs.existsSync('./dist/index.js');
|
||||
|
||||
console.log(`📁 dist directory: ${distExists ? '✅' : '❌'}`);
|
||||
console.log(`📄 index.js: ${indexExists ? '✅' : '❌'}`);
|
||||
|
||||
if (distExists) {
|
||||
const distFiles = fs.readdirSync('./dist');
|
||||
console.log(`📦 Generated files: ${distFiles.length}`);
|
||||
distFiles.forEach(file => {
|
||||
console.log(` - ${file}`);
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('⚠️ Could not check build output:', e.message);
|
||||
}
|
||||
} else {
|
||||
console.log('❌ Build: FAILED');
|
||||
console.log('Build errors:');
|
||||
console.log(buildResult.stderr);
|
||||
}
|
||||
console.log('');
|
||||
} else {
|
||||
console.log('⏭️ Skipping build test due to type errors\n');
|
||||
}
|
||||
|
||||
// Step 4: Summary
|
||||
console.log('📊 Final Summary');
|
||||
console.log('================');
|
||||
console.log(`Type Check: ${typeCheckResult.code === 0 ? '✅ PASSED' : '❌ FAILED'}`);
|
||||
|
||||
if (typeCheckResult.code === 0) {
|
||||
console.log('🎉 TickTick MCP Server is ready for testing!');
|
||||
console.log('');
|
||||
console.log('Next steps:');
|
||||
console.log(' 1. Test setup: node dist/index.js --setup');
|
||||
console.log(' 2. Test demo mode: node dist/index.js --demo');
|
||||
console.log(' 3. Publish to NPM (when ready)');
|
||||
} else {
|
||||
console.log('🔧 Please fix the type errors before proceeding.');
|
||||
}
|
||||
console.log('');
|
||||
|
||||
} catch (error) {
|
||||
console.error('💥 Script error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
74
check-types.js
Normal file
74
check-types.js
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const { spawn } = require('child_process');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
function runTypeCheck() {
|
||||
return new Promise((resolve, reject) => {
|
||||
console.log('Running TypeScript type checking...');
|
||||
|
||||
const tsc = spawn('npx', ['tsc', '--noEmit', '--pretty'], {
|
||||
stdio: 'pipe',
|
||||
shell: true,
|
||||
cwd: process.cwd()
|
||||
});
|
||||
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
|
||||
tsc.stdout.on('data', (data) => {
|
||||
stdout += data.toString();
|
||||
});
|
||||
|
||||
tsc.stderr.on('data', (data) => {
|
||||
stderr += data.toString();
|
||||
});
|
||||
|
||||
tsc.on('close', (code) => {
|
||||
resolve({
|
||||
code,
|
||||
stdout,
|
||||
stderr,
|
||||
success: code === 0
|
||||
});
|
||||
});
|
||||
|
||||
tsc.on('error', (error) => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log('TickTick MCP Server - TypeScript Type Checking');
|
||||
console.log('==============================================');
|
||||
|
||||
try {
|
||||
const result = await runTypeCheck();
|
||||
|
||||
if (result.success) {
|
||||
console.log('✅ No TypeScript errors found!');
|
||||
console.log('\nType checking completed successfully.');
|
||||
} else {
|
||||
console.log('❌ TypeScript errors found:');
|
||||
console.log('\nSTDOUT:');
|
||||
console.log(result.stdout);
|
||||
console.log('\nSTDERR:');
|
||||
console.log(result.stderr);
|
||||
}
|
||||
|
||||
console.log('\n==============================================');
|
||||
console.log(`Exit code: ${result.code}`);
|
||||
|
||||
process.exit(result.code);
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Error running type check:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
main();
|
||||
}
|
||||
11
claude-desktop-config.json
Normal file
11
claude-desktop-config.json
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"mcpServers": {
|
||||
"ticktick": {
|
||||
"command": "node",
|
||||
"args": ["/Users/takashishibata/Desktop/creative-lab/mcp-research/ticktick-mcp-server/dist/index.js", "--demo"],
|
||||
"env": {
|
||||
"TICKTICK_DEMO_MODE": "true"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
67
debug-types.js
Normal file
67
debug-types.js
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
console.log('TypeScript Files Analysis');
|
||||
console.log('========================');
|
||||
|
||||
// Function to read and analyze TypeScript files
|
||||
function analyzeTypeScriptFile(filePath) {
|
||||
try {
|
||||
const content = fs.readFileSync(filePath, 'utf8');
|
||||
const lines = content.split('\n');
|
||||
|
||||
console.log(`\n📁 ${filePath}`);
|
||||
console.log(`Lines: ${lines.length}`);
|
||||
|
||||
// Check for imports
|
||||
const imports = lines.filter(line => line.trim().startsWith('import'));
|
||||
if (imports.length > 0) {
|
||||
console.log('Imports:');
|
||||
imports.forEach(imp => console.log(` ${imp.trim()}`));
|
||||
}
|
||||
|
||||
// Check for exports
|
||||
const exports = lines.filter(line => line.trim().startsWith('export'));
|
||||
if (exports.length > 0) {
|
||||
console.log('Exports:');
|
||||
exports.slice(0, 5).forEach(exp => console.log(` ${exp.trim()}`));
|
||||
if (exports.length > 5) {
|
||||
console.log(` ... and ${exports.length - 5} more`);
|
||||
}
|
||||
}
|
||||
|
||||
// Check for interface definitions
|
||||
const interfaces = lines.filter(line => line.trim().startsWith('export interface') || line.trim().startsWith('interface'));
|
||||
if (interfaces.length > 0) {
|
||||
console.log('Interfaces:');
|
||||
interfaces.forEach(int => console.log(` ${int.trim()}`));
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.log(`❌ Error reading ${filePath}: ${error.message}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// List of files to analyze
|
||||
const filesToAnalyze = [
|
||||
'src/types/ticktick.ts',
|
||||
'src/types/api-interface.ts',
|
||||
'src/auth/ticktick-api.ts',
|
||||
'src/demo/mock-data.ts',
|
||||
'src/server.ts',
|
||||
'src/index.ts'
|
||||
];
|
||||
|
||||
// Analyze each file
|
||||
filesToAnalyze.forEach(file => {
|
||||
if (fs.existsSync(file)) {
|
||||
analyzeTypeScriptFile(file);
|
||||
} else {
|
||||
console.log(`❌ File not found: ${file}`);
|
||||
}
|
||||
});
|
||||
|
||||
console.log('\n========================');
|
||||
console.log('Analysis complete');
|
||||
120
diagnose-build.js
Normal file
120
diagnose-build.js
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const { execSync, spawn } = require('child_process');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
async function runDiagnosis() {
|
||||
console.log('TickTick MCP Server - Build Diagnosis');
|
||||
console.log('====================================');
|
||||
|
||||
const projectDir = '/Users/takashishibata/Desktop/creative-lab/mcp-research/ticktick-mcp-server';
|
||||
|
||||
try {
|
||||
process.chdir(projectDir);
|
||||
console.log(`Working directory: ${process.cwd()}`);
|
||||
|
||||
// Step 1: Check dependencies
|
||||
console.log('\n1. Checking dependencies...');
|
||||
if (!fs.existsSync('node_modules')) {
|
||||
console.log('Installing dependencies...');
|
||||
execSync('npm install', { stdio: 'inherit' });
|
||||
}
|
||||
console.log('✅ Dependencies checked');
|
||||
|
||||
// Step 2: Clear any existing dist
|
||||
console.log('\n2. Cleaning build output...');
|
||||
if (fs.existsSync('dist')) {
|
||||
execSync('rm -rf dist', { stdio: 'inherit' });
|
||||
}
|
||||
console.log('✅ Build output cleaned');
|
||||
|
||||
// Step 3: Check TypeScript configuration
|
||||
console.log('\n3. Checking TypeScript configuration...');
|
||||
const tsConfig = JSON.parse(fs.readFileSync('tsconfig.json', 'utf8'));
|
||||
console.log(`Target: ${tsConfig.compilerOptions.target}`);
|
||||
console.log(`Module: ${tsConfig.compilerOptions.module}`);
|
||||
console.log(`Module Resolution: ${tsConfig.compilerOptions.moduleResolution}`);
|
||||
console.log('✅ TypeScript configuration loaded');
|
||||
|
||||
// Step 4: Run type checking
|
||||
console.log('\n4. Running TypeScript type checking...');
|
||||
try {
|
||||
const typeCheckOutput = execSync('npx tsc --noEmit --pretty', {
|
||||
encoding: 'utf8',
|
||||
stdio: 'pipe'
|
||||
});
|
||||
console.log('✅ Type checking passed');
|
||||
if (typeCheckOutput.trim()) {
|
||||
console.log('Type check output:', typeCheckOutput);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('❌ Type checking failed:');
|
||||
console.log('--- STDOUT ---');
|
||||
console.log(error.stdout || '(no stdout)');
|
||||
console.log('--- STDERR ---');
|
||||
console.log(error.stderr || '(no stderr)');
|
||||
|
||||
// Still try to continue with build
|
||||
console.log('\nContinuing with build attempt...');
|
||||
}
|
||||
|
||||
// Step 5: Run build
|
||||
console.log('\n5. Running build...');
|
||||
try {
|
||||
const buildOutput = execSync('npx tsc', {
|
||||
encoding: 'utf8',
|
||||
stdio: 'pipe'
|
||||
});
|
||||
console.log('✅ Build completed successfully');
|
||||
|
||||
// Check what was built
|
||||
if (fs.existsSync('dist')) {
|
||||
const distFiles = fs.readdirSync('dist');
|
||||
console.log(`📁 Built ${distFiles.length} files:`);
|
||||
distFiles.slice(0, 10).forEach(file => {
|
||||
console.log(` ${file}`);
|
||||
});
|
||||
if (distFiles.length > 10) {
|
||||
console.log(` ... and ${distFiles.length - 10} more files`);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.log('❌ Build failed:');
|
||||
console.log('--- STDOUT ---');
|
||||
console.log(error.stdout || '(no stdout)');
|
||||
console.log('--- STDERR ---');
|
||||
console.log(error.stderr || '(no stderr)');
|
||||
}
|
||||
|
||||
// Step 6: Test the built output
|
||||
if (fs.existsSync('dist/index.js')) {
|
||||
console.log('\n6. Testing built output...');
|
||||
try {
|
||||
const testOutput = execSync('node dist/index.js --help', {
|
||||
encoding: 'utf8',
|
||||
stdio: 'pipe',
|
||||
timeout: 5000
|
||||
});
|
||||
console.log('✅ Built output can be executed');
|
||||
} catch (error) {
|
||||
// This might be expected if the script doesn't support --help
|
||||
console.log('⚠️ Build output test had issues (might be expected)');
|
||||
console.log(error.stdout || error.stderr || error.message);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\n====================================');
|
||||
console.log('Diagnosis complete');
|
||||
|
||||
} catch (error) {
|
||||
console.error('💥 Unexpected error:', error.message);
|
||||
console.error(error.stack);
|
||||
}
|
||||
}
|
||||
|
||||
// Run if this file is executed directly
|
||||
if (require.main === module) {
|
||||
runDiagnosis();
|
||||
}
|
||||
95
oauth-callback-server.js
Normal file
95
oauth-callback-server.js
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
import http from 'http';
|
||||
import url from 'url';
|
||||
|
||||
console.log('🚀 TickTick OAuth コールバックサーバーを起動中...');
|
||||
|
||||
const server = http.createServer((req, res) => {
|
||||
const parsedUrl = url.parse(req.url, true);
|
||||
|
||||
if (parsedUrl.pathname === '/callback' || parsedUrl.pathname === '/api/ticktick/callback') {
|
||||
const { code, error } = parsedUrl.query;
|
||||
|
||||
if (error) {
|
||||
res.writeHead(400, {'Content-Type': 'text/html; charset=utf-8'});
|
||||
res.end(`
|
||||
<html>
|
||||
<head><title>認証エラー</title></head>
|
||||
<body style="font-family: Arial, sans-serif; margin: 50px; text-align: center;">
|
||||
<h1>❌ 認証エラー</h1>
|
||||
<p>エラー: ${error}</p>
|
||||
<p>ブラウザを閉じて、セットアップを再実行してください。</p>
|
||||
</body>
|
||||
</html>
|
||||
`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (code) {
|
||||
res.writeHead(200, {'Content-Type': 'text/html; charset=utf-8'});
|
||||
res.end(`
|
||||
<html>
|
||||
<head><title>認証成功</title></head>
|
||||
<body style="font-family: Arial, sans-serif; margin: 50px; text-align: center;">
|
||||
<h1>✅ 認証成功!</h1>
|
||||
<div style="background: #f0f0f0; padding: 20px; margin: 20px; border-radius: 8px;">
|
||||
<h3>認証コード:</h3>
|
||||
<code style="background: white; padding: 10px; display: block; margin: 10px; font-size: 14px; word-break: break-all;">${code}</code>
|
||||
</div>
|
||||
<p>このコードをターミナルにコピー&ペーストしてください。</p>
|
||||
<p>完了後、このブラウザを閉じてください。</p>
|
||||
</body>
|
||||
</html>
|
||||
`);
|
||||
|
||||
console.log('✅ 認証コードを受信しました!');
|
||||
console.log('📋 認証コード:', code);
|
||||
console.log('');
|
||||
console.log('👆 このコードをターミナルの入力プロンプトにペーストしてください');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// その他のリクエスト
|
||||
res.writeHead(404, {'Content-Type': 'text/html; charset=utf-8'});
|
||||
res.end(`
|
||||
<html>
|
||||
<head><title>TickTick OAuth Callback</title></head>
|
||||
<body style="font-family: Arial, sans-serif; margin: 50px; text-align: center;">
|
||||
<h1>🔗 TickTick OAuth Callback Server</h1>
|
||||
<p>このサーバーはTickTick認証用です。</p>
|
||||
<p>ブラウザでTickTick認証を完了してください。</p>
|
||||
</body>
|
||||
</html>
|
||||
`);
|
||||
});
|
||||
|
||||
const PORT = 3000;
|
||||
server.listen(PORT, () => {
|
||||
console.log(`📡 OAuth コールバックサーバーが起動しました`);
|
||||
console.log(`🌐 URL: http://localhost:${PORT}/callback`);
|
||||
console.log(`🌐 URL: http://localhost:${PORT}/api/ticktick/callback`);
|
||||
console.log('');
|
||||
console.log('✨ 準備完了!別のターミナルで以下を実行してください:');
|
||||
console.log(' node dist/index.js --setup');
|
||||
console.log('');
|
||||
console.log('⏹️ 終了するには Ctrl+C を押してください');
|
||||
});
|
||||
|
||||
// Graceful shutdown
|
||||
process.on('SIGINT', () => {
|
||||
console.log('\n🛑 サーバーを終了しています...');
|
||||
server.close(() => {
|
||||
console.log('✅ サーバーが正常に終了しました');
|
||||
process.exit(0);
|
||||
});
|
||||
});
|
||||
|
||||
process.on('SIGTERM', () => {
|
||||
console.log('\n🛑 サーバーを終了しています...');
|
||||
server.close(() => {
|
||||
console.log('✅ サーバーが正常に終了しました');
|
||||
process.exit(0);
|
||||
});
|
||||
});
|
||||
5946
package-lock.json
generated
Normal file
5946
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
94
package.json
Normal file
94
package.json
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
{
|
||||
"name": "@ticktick-ecosystem/mcp-server",
|
||||
"version": "1.0.0",
|
||||
"description": "Model Context Protocol server for TickTick task management integration with AI assistants like Claude",
|
||||
"main": "dist/index.js",
|
||||
"bin": {
|
||||
"ticktick-mcp-server": "dist/index.js"
|
||||
},
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"dev": "tsx src/index.ts",
|
||||
"start": "node dist/index.js",
|
||||
"demo": "node dist/index.js --demo",
|
||||
"setup": "node dist/index.js --setup",
|
||||
"test": "jest",
|
||||
"lint": "eslint src/**/*.ts",
|
||||
"format": "prettier --write src/**/*.ts",
|
||||
"setup-env": "chmod +x scripts/setup-env.sh && ./scripts/setup-env.sh",
|
||||
"test-oauth": "npm run build && node scripts/test-oauth.js",
|
||||
"postinstall": "npm run build",
|
||||
"prepare": "npm run build"
|
||||
},
|
||||
"keywords": [
|
||||
"mcp",
|
||||
"model-context-protocol",
|
||||
"ticktick",
|
||||
"task-management",
|
||||
"ai",
|
||||
"automation",
|
||||
"productivity",
|
||||
"claude",
|
||||
"assistant",
|
||||
"todo",
|
||||
"project-management",
|
||||
"oauth",
|
||||
"api"
|
||||
],
|
||||
"author": {
|
||||
"name": "TickTick Ecosystem",
|
||||
"email": "support@ticktick-ecosystem.com",
|
||||
"url": "https://github.com/ticktick-ecosystem"
|
||||
},
|
||||
"license": "MIT",
|
||||
"files": [
|
||||
"dist/",
|
||||
"README.md",
|
||||
"AUTHENTICATION.md",
|
||||
"TESTING.md",
|
||||
"LICENSE",
|
||||
".env.example",
|
||||
"claude-desktop-config.json"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"os": [
|
||||
"darwin",
|
||||
"linux",
|
||||
"win32"
|
||||
],
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^0.4.0",
|
||||
"axios": "^1.6.0",
|
||||
"zod": "^3.22.0",
|
||||
"dotenv": "^16.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
||||
"@typescript-eslint/parser": "^6.0.0",
|
||||
"eslint": "^8.0.0",
|
||||
"jest": "^29.0.0",
|
||||
"prettier": "^3.0.0",
|
||||
"tsx": "^4.0.0",
|
||||
"typescript": "^5.0.0"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/ticktick-ecosystem/mcp-server.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/ticktick-ecosystem/mcp-server/issues"
|
||||
},
|
||||
"homepage": "https://github.com/ticktick-ecosystem/mcp-server#readme",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ticktick-ecosystem"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"registry": "https://registry.npmjs.org/"
|
||||
}
|
||||
}
|
||||
105
quick-build-check.js
Normal file
105
quick-build-check.js
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
const { spawn } = require('child_process');
|
||||
const path = require('path');
|
||||
|
||||
// Project directory
|
||||
const projectDir = '/Users/takashishibata/Desktop/creative-lab/mcp-research/ticktick-mcp-server';
|
||||
|
||||
console.log('🔍 Quick TypeScript Build Check');
|
||||
console.log('===============================');
|
||||
|
||||
process.chdir(projectDir);
|
||||
console.log(`📂 Working directory: ${process.cwd()}`);
|
||||
console.log('');
|
||||
|
||||
// Function to run command and capture output
|
||||
function runCommand(command, args = []) {
|
||||
return new Promise((resolve, reject) => {
|
||||
console.log(`⚡ Running: ${command} ${args.join(' ')}`);
|
||||
|
||||
const child = spawn(command, args, {
|
||||
stdio: 'pipe',
|
||||
shell: true
|
||||
});
|
||||
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
|
||||
child.stdout.on('data', (data) => {
|
||||
stdout += data.toString();
|
||||
});
|
||||
|
||||
child.stderr.on('data', (data) => {
|
||||
stderr += data.toString();
|
||||
});
|
||||
|
||||
child.on('close', (code) => {
|
||||
console.log(`✅ Exit code: ${code}`);
|
||||
resolve({ code, stdout, stderr });
|
||||
});
|
||||
|
||||
child.on('error', (error) => {
|
||||
console.error(`❌ Command error: ${error}`);
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
// Check TypeScript version
|
||||
console.log('📋 Checking TypeScript version...');
|
||||
const tscVersion = await runCommand('npx', ['tsc', '--version']);
|
||||
console.log(`TypeScript: ${tscVersion.stdout.trim()}`);
|
||||
console.log('');
|
||||
|
||||
// Check syntax only (no emit)
|
||||
console.log('🔍 Checking TypeScript syntax (no emit)...');
|
||||
const syntaxCheck = await runCommand('npx', ['tsc', '--noEmit']);
|
||||
|
||||
if (syntaxCheck.code === 0) {
|
||||
console.log('✅ TypeScript syntax check: PASSED');
|
||||
} else {
|
||||
console.log('❌ TypeScript syntax check: FAILED');
|
||||
console.log('Errors:');
|
||||
console.log(syntaxCheck.stderr);
|
||||
}
|
||||
console.log('');
|
||||
|
||||
// Attempt full build
|
||||
console.log('🏗️ Attempting full build...');
|
||||
const buildResult = await runCommand('npm', ['run', 'build']);
|
||||
|
||||
if (buildResult.code === 0) {
|
||||
console.log('✅ Build: SUCCESS');
|
||||
console.log('📦 Checking build output...');
|
||||
|
||||
// Check if dist directory exists
|
||||
try {
|
||||
const fs = require('fs');
|
||||
const distExists = fs.existsSync('./dist');
|
||||
const indexExists = fs.existsSync('./dist/index.js');
|
||||
|
||||
console.log(`📁 dist directory: ${distExists ? '✅' : '❌'}`);
|
||||
console.log(`📄 index.js: ${indexExists ? '✅' : '❌'}`);
|
||||
|
||||
if (distExists) {
|
||||
const distFiles = fs.readdirSync('./dist');
|
||||
console.log(`📁 Build files: ${distFiles.length} files created`);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('⚠️ Could not check build output:', e.message);
|
||||
}
|
||||
} else {
|
||||
console.log('❌ Build: FAILED');
|
||||
console.log('Build errors:');
|
||||
console.log(buildResult.stderr);
|
||||
console.log('Build output:');
|
||||
console.log(buildResult.stdout);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('💥 Script error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
85
run-build.mjs
Normal file
85
run-build.mjs
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
import { spawn } from 'child_process';
|
||||
import { promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
function runCommand(command, args = [], options = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
console.log(`Running: ${command} ${args.join(' ')}`);
|
||||
|
||||
const child = spawn(command, args, {
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
shell: true,
|
||||
...options
|
||||
});
|
||||
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
|
||||
child.stdout.on('data', (data) => {
|
||||
const output = data.toString();
|
||||
stdout += output;
|
||||
process.stdout.write(output);
|
||||
});
|
||||
|
||||
child.stderr.on('data', (data) => {
|
||||
const output = data.toString();
|
||||
stderr += output;
|
||||
process.stderr.write(output);
|
||||
});
|
||||
|
||||
child.on('close', (code) => {
|
||||
resolve({
|
||||
code,
|
||||
stdout,
|
||||
stderr,
|
||||
success: code === 0
|
||||
});
|
||||
});
|
||||
|
||||
child.on('error', (error) => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log('TickTick MCP Server - Build Test');
|
||||
console.log('=================================');
|
||||
|
||||
try {
|
||||
// Check TypeScript first
|
||||
console.log('\n1. TypeScript type checking...');
|
||||
const typeResult = await runCommand('npx', ['tsc', '--noEmit']);
|
||||
|
||||
if (!typeResult.success) {
|
||||
console.log('\n❌ TypeScript type errors found!');
|
||||
console.log('Fix these errors before proceeding with build.');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('✅ TypeScript type checking passed');
|
||||
|
||||
// Build the project
|
||||
console.log('\n2. Building project...');
|
||||
const buildResult = await runCommand('npx', ['tsc']);
|
||||
|
||||
if (buildResult.success) {
|
||||
console.log('\n✅ Build completed successfully!');
|
||||
|
||||
// Check dist directory
|
||||
try {
|
||||
const distFiles = await fs.readdir('dist');
|
||||
console.log('\n📁 Built files:', distFiles);
|
||||
} catch (error) {
|
||||
console.log('Note: Could not read dist directory');
|
||||
}
|
||||
} else {
|
||||
console.log('\n❌ Build failed!');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('\n💥 Error:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
15
run-tsc.js
Normal file
15
run-tsc.js
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const { execSync } = require('child_process');
|
||||
|
||||
try {
|
||||
console.log('Running TypeScript compilation...');
|
||||
execSync('npx tsc', {
|
||||
cwd: process.cwd(),
|
||||
stdio: 'inherit'
|
||||
});
|
||||
console.log('TypeScript compilation successful!');
|
||||
} catch (error) {
|
||||
console.error('TypeScript compilation failed with exit code:', error.status);
|
||||
process.exit(error.status);
|
||||
}
|
||||
54
scripts/setup-env.sh
Normal file
54
scripts/setup-env.sh
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
#!/bin/bash
|
||||
|
||||
# TickTick MCP Server Environment Setup Script
|
||||
|
||||
echo "🚀 TickTick MCP Server Environment Setup"
|
||||
echo "========================================"
|
||||
echo ""
|
||||
|
||||
# Check if .env already exists
|
||||
if [ -f ".env" ]; then
|
||||
echo "⚠️ .env file already exists!"
|
||||
read -p "Do you want to overwrite it? (y/N): " overwrite
|
||||
if [[ $overwrite != "y" && $overwrite != "Y" ]]; then
|
||||
echo "❌ Setup cancelled"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Copy example file
|
||||
cp .env.example .env
|
||||
echo "✅ Created .env file from template"
|
||||
echo ""
|
||||
|
||||
# Prompt for credentials
|
||||
echo "📋 Please enter your TickTick API credentials:"
|
||||
echo " (Get these from https://developer.ticktick.com/)"
|
||||
echo ""
|
||||
|
||||
read -p "Client ID: " client_id
|
||||
read -p "Client Secret: " client_secret
|
||||
read -p "Redirect URI (default: http://localhost:3000/callback): " redirect_uri
|
||||
|
||||
# Set default redirect URI if empty
|
||||
if [ -z "$redirect_uri" ]; then
|
||||
redirect_uri="http://localhost:3000/callback"
|
||||
fi
|
||||
|
||||
# Update .env file
|
||||
sed -i.bak "s/your_client_id_here/$client_id/g" .env
|
||||
sed -i.bak "s/your_client_secret_here/$client_secret/g" .env
|
||||
sed -i.bak "s|http://localhost:3000/callback|$redirect_uri|g" .env
|
||||
|
||||
# Remove backup file
|
||||
rm .env.bak
|
||||
|
||||
echo ""
|
||||
echo "✅ Environment configured successfully!"
|
||||
echo ""
|
||||
echo "🔧 Next steps:"
|
||||
echo "1. Run: npm run dev"
|
||||
echo "2. Follow the OAuth URL to authenticate"
|
||||
echo "3. Test with MCP Inspector or Claude Desktop"
|
||||
echo ""
|
||||
echo "📖 For more info, see README.md"
|
||||
114
scripts/test-oauth.js
Normal file
114
scripts/test-oauth.js
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* TickTick OAuth Test Helper
|
||||
* This script helps you complete the OAuth flow and get access tokens
|
||||
*/
|
||||
|
||||
import { createServer } from 'http';
|
||||
import { parse } from 'url';
|
||||
import { TickTickAuth } from '../dist/auth/ticktick-auth.js';
|
||||
|
||||
// Load environment variables
|
||||
import dotenv from 'dotenv';
|
||||
dotenv.config();
|
||||
|
||||
const config = {
|
||||
clientId: process.env.TICKTICK_CLIENT_ID,
|
||||
clientSecret: process.env.TICKTICK_CLIENT_SECRET,
|
||||
redirectUri: process.env.TICKTICK_REDIRECT_URI || 'http://localhost:3000/callback',
|
||||
};
|
||||
|
||||
if (!config.clientId || !config.clientSecret) {
|
||||
console.error('❌ Missing TICKTICK_CLIENT_ID or TICKTICK_CLIENT_SECRET');
|
||||
console.error('Run: npm run setup-env first');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const auth = new TickTickAuth(config);
|
||||
|
||||
// Create a simple HTTP server to handle OAuth callback
|
||||
const server = createServer(async (req, res) => {
|
||||
const urlParts = parse(req.url, true);
|
||||
|
||||
if (urlParts.pathname === '/callback') {
|
||||
const { code, error } = urlParts.query;
|
||||
|
||||
if (error) {
|
||||
res.writeHead(400, { 'Content-Type': 'text/html' });
|
||||
res.end(`
|
||||
<html>
|
||||
<body>
|
||||
<h1>❌ OAuth Error</h1>
|
||||
<p>Error: ${error}</p>
|
||||
<p>Please try again.</p>
|
||||
</body>
|
||||
</html>
|
||||
`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (code) {
|
||||
try {
|
||||
console.log('\n🔄 Exchanging authorization code for tokens...');
|
||||
const tokens = await auth.exchangeCodeForToken(code);
|
||||
|
||||
console.log('\n✅ OAuth Success!');
|
||||
console.log('📋 Add these to your .env file:');
|
||||
console.log(`TICKTICK_ACCESS_TOKEN=${tokens.access_token}`);
|
||||
console.log(`TICKTICK_REFRESH_TOKEN=${tokens.refresh_token}`);
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'text/html' });
|
||||
res.end(`
|
||||
<html>
|
||||
<body>
|
||||
<h1>✅ OAuth Success!</h1>
|
||||
<p>Your TickTick MCP Server is now authenticated.</p>
|
||||
<p>Check your terminal for the access tokens.</p>
|
||||
<p>You can close this window and stop the server (Ctrl+C).</p>
|
||||
</body>
|
||||
</html>
|
||||
`);
|
||||
|
||||
console.log('\n🎉 Authentication complete!');
|
||||
console.log('You can now stop this server (Ctrl+C) and test your MCP server.');
|
||||
|
||||
} catch (error) {
|
||||
console.error('\n❌ Token exchange failed:', error.message);
|
||||
res.writeHead(500, { 'Content-Type': 'text/html' });
|
||||
res.end(`
|
||||
<html>
|
||||
<body>
|
||||
<h1>❌ Token Exchange Failed</h1>
|
||||
<p>Error: ${error.message}</p>
|
||||
</body>
|
||||
</html>
|
||||
`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
||||
res.end('Not found');
|
||||
}
|
||||
});
|
||||
|
||||
// Start server
|
||||
const port = new URL(config.redirectUri).port || 3000;
|
||||
server.listen(port, () => {
|
||||
console.log('🚀 TickTick OAuth Helper Started');
|
||||
console.log('===============================');
|
||||
console.log(`📡 Listening on port ${port}`);
|
||||
console.log('');
|
||||
console.log('🔗 Open this URL in your browser to authenticate:');
|
||||
console.log(auth.getAuthorizationUrl());
|
||||
console.log('');
|
||||
console.log('💡 After authentication, you\'ll get access tokens to add to your .env file');
|
||||
console.log('⏹️ Press Ctrl+C to stop this server');
|
||||
});
|
||||
|
||||
// Handle graceful shutdown
|
||||
process.on('SIGINT', () => {
|
||||
console.log('\n\n🛑 OAuth helper stopped');
|
||||
server.close();
|
||||
process.exit(0);
|
||||
});
|
||||
84
simple-build-test.js
Normal file
84
simple-build-test.js
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
const { execSync } = require('child_process');
|
||||
const fs = require('fs');
|
||||
|
||||
console.log('TickTick MCP Server - Simple Build Test');
|
||||
console.log('=======================================');
|
||||
|
||||
try {
|
||||
// Change to project directory
|
||||
process.chdir('/Users/takashishibata/Desktop/creative-lab/mcp-research/ticktick-mcp-server');
|
||||
|
||||
console.log('Current directory:', process.cwd());
|
||||
console.log('');
|
||||
|
||||
// Check if package.json exists
|
||||
if (fs.existsSync('package.json')) {
|
||||
console.log('✅ package.json found');
|
||||
} else {
|
||||
console.log('❌ package.json not found');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Check if tsconfig.json exists
|
||||
if (fs.existsSync('tsconfig.json')) {
|
||||
console.log('✅ tsconfig.json found');
|
||||
} else {
|
||||
console.log('❌ tsconfig.json not found');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Check if node_modules exists
|
||||
if (fs.existsSync('node_modules')) {
|
||||
console.log('✅ node_modules found');
|
||||
} else {
|
||||
console.log('⚠️ node_modules not found, installing...');
|
||||
execSync('npm install', { stdio: 'inherit' });
|
||||
}
|
||||
|
||||
console.log('\n1. Running TypeScript type check...');
|
||||
console.log('-----------------------------------');
|
||||
|
||||
try {
|
||||
const output = execSync('npx tsc --noEmit', {
|
||||
encoding: 'utf8',
|
||||
stdio: 'pipe'
|
||||
});
|
||||
console.log('✅ TypeScript type check passed');
|
||||
if (output.trim()) {
|
||||
console.log('Output:', output);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('❌ TypeScript type check failed:');
|
||||
console.log(error.stdout);
|
||||
console.log(error.stderr);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('\n2. Running build...');
|
||||
console.log('-------------------');
|
||||
|
||||
try {
|
||||
const output = execSync('npx tsc', {
|
||||
encoding: 'utf8',
|
||||
stdio: 'pipe'
|
||||
});
|
||||
console.log('✅ Build completed successfully');
|
||||
if (output.trim()) {
|
||||
console.log('Output:', output);
|
||||
}
|
||||
|
||||
// Check dist directory
|
||||
if (fs.existsSync('dist')) {
|
||||
const files = fs.readdirSync('dist');
|
||||
console.log('📁 Built files:', files);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.log('❌ Build failed:');
|
||||
console.log(error.stdout);
|
||||
console.log(error.stderr);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('💥 Unexpected error:', error.message);
|
||||
}
|
||||
57
simple-type-check.js
Normal file
57
simple-type-check.js
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
const { exec } = require('child_process');
|
||||
const path = require('path');
|
||||
|
||||
const projectDir = '/Users/takashishibata/Desktop/creative-lab/mcp-research/ticktick-mcp-server';
|
||||
|
||||
console.log('🔍 Simple TypeScript Type Check');
|
||||
console.log('================================');
|
||||
|
||||
process.chdir(projectDir);
|
||||
console.log(`📂 Working in: ${process.cwd()}\n`);
|
||||
|
||||
// Run TypeScript type check only
|
||||
console.log('⚡ Running TypeScript type check (no emit)...');
|
||||
exec('npx tsc --noEmit', (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
console.log('❌ TypeScript errors found:');
|
||||
console.log('---------------------------');
|
||||
console.log(stderr);
|
||||
console.log('\n📊 Error Analysis:');
|
||||
|
||||
// Count different types of errors
|
||||
const errorLines = stderr.split('\n').filter(line => line.includes('error TS'));
|
||||
console.log(`Total errors: ${errorLines.length}`);
|
||||
|
||||
// Group by error type
|
||||
const errorTypes = {};
|
||||
errorLines.forEach(line => {
|
||||
const match = line.match(/error TS(\d+):/);
|
||||
if (match) {
|
||||
const code = match[1];
|
||||
errorTypes[code] = (errorTypes[code] || 0) + 1;
|
||||
}
|
||||
});
|
||||
|
||||
console.log('\nError types:');
|
||||
Object.entries(errorTypes).forEach(([code, count]) => {
|
||||
console.log(` TS${code}: ${count} errors`);
|
||||
});
|
||||
|
||||
} else {
|
||||
console.log('✅ No TypeScript errors found!');
|
||||
console.log('Ready to build...\n');
|
||||
|
||||
// If no type errors, try building
|
||||
console.log('🏗️ Running full build...');
|
||||
exec('npm run build', (buildError, buildStdout, buildStderr) => {
|
||||
if (buildError) {
|
||||
console.log('❌ Build failed:');
|
||||
console.log(buildStderr);
|
||||
} else {
|
||||
console.log('✅ Build successful!');
|
||||
console.log('📦 Output:');
|
||||
console.log(buildStdout);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
235
src/auth/ticktick-api.ts
Normal file
235
src/auth/ticktick-api.ts
Normal file
|
|
@ -0,0 +1,235 @@
|
|||
import axios, { AxiosInstance } from 'axios';
|
||||
import { TickTickAuth } from './ticktick-auth.js';
|
||||
import {
|
||||
Task,
|
||||
Project,
|
||||
TaskFilter,
|
||||
APIResponse,
|
||||
} from '../types/ticktick.js';
|
||||
import { ITickTickAPI } from '../types/api-interface.js';
|
||||
|
||||
export class TickTickAPI implements ITickTickAPI {
|
||||
private api: AxiosInstance;
|
||||
private auth: TickTickAuth;
|
||||
private baseURL = 'https://api.ticktick.com/open/v1';
|
||||
|
||||
constructor(auth: TickTickAuth) {
|
||||
this.auth = auth;
|
||||
this.api = axios.create({
|
||||
baseURL: this.baseURL,
|
||||
timeout: 10000,
|
||||
});
|
||||
|
||||
// Add request interceptor to include auth headers
|
||||
this.api.interceptors.request.use((config) => {
|
||||
const headers = this.auth.getAuthHeaders();
|
||||
Object.assign(config.headers, headers);
|
||||
return config;
|
||||
});
|
||||
|
||||
// Add response interceptor to handle token refresh
|
||||
this.api.interceptors.response.use(
|
||||
(response) => response,
|
||||
async (error) => {
|
||||
if (error.response?.status === 401) {
|
||||
try {
|
||||
await this.auth.refreshAccessToken();
|
||||
// Retry the original request
|
||||
const originalRequest = error.config;
|
||||
const headers = this.auth.getAuthHeaders();
|
||||
Object.assign(originalRequest.headers, headers);
|
||||
return this.api.request(originalRequest);
|
||||
} catch (refreshError) {
|
||||
throw new Error('Authentication failed. Please re-authenticate.');
|
||||
}
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Task Management Methods
|
||||
|
||||
/**
|
||||
* Get all tasks with optional filtering
|
||||
*/
|
||||
async getTasks(filter: any = {}): Promise<Task[]> {
|
||||
try {
|
||||
const params = new URLSearchParams();
|
||||
|
||||
if (filter.projectId) params.append('projectId', filter.projectId);
|
||||
if (filter.completed !== undefined) params.append('completed', filter.completed.toString());
|
||||
if (filter.startDate) params.append('startDate', filter.startDate);
|
||||
if (filter.endDate) params.append('endDate', filter.endDate);
|
||||
if (filter.limit) params.append('limit', filter.limit.toString());
|
||||
if (filter.offset) params.append('offset', filter.offset.toString());
|
||||
|
||||
const response = await this.api.get(`/task?${params.toString()}`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to get tasks: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get task by ID
|
||||
*/
|
||||
async getTask(taskId: string): Promise<Task> {
|
||||
try {
|
||||
const response = await this.api.get(`/task/${taskId}`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to get task: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new task
|
||||
*/
|
||||
async createTask(taskData: any): Promise<Task> {
|
||||
try {
|
||||
const response = await this.api.post('/task', taskData);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to create task: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing task
|
||||
*/
|
||||
async updateTask(taskData: any): Promise<Task> {
|
||||
try {
|
||||
const { id, ...updateData } = taskData;
|
||||
const response = await this.api.post(`/task/${id}`, updateData);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to update task: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Complete a task
|
||||
*/
|
||||
async completeTask(taskId: string): Promise<Task> {
|
||||
try {
|
||||
const response = await this.api.post(`/task/${taskId}/complete`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to complete task: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a task
|
||||
*/
|
||||
async deleteTask(taskId: string): Promise<void> {
|
||||
try {
|
||||
await this.api.delete(`/task/${taskId}`);
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to delete task: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Project Management Methods
|
||||
|
||||
/**
|
||||
* Get all projects
|
||||
*/
|
||||
async getProjects(): Promise<Project[]> {
|
||||
try {
|
||||
const response = await this.api.get('/project');
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to get projects: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get project by ID
|
||||
*/
|
||||
async getProject(projectId: string): Promise<Project> {
|
||||
try {
|
||||
const response = await this.api.get(`/project/${projectId}`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to get project: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new project
|
||||
*/
|
||||
async createProject(projectData: any): Promise<Project> {
|
||||
try {
|
||||
const response = await this.api.post('/project', projectData);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to create project: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing project
|
||||
*/
|
||||
async updateProject(projectId: string, projectData: any): Promise<Project> {
|
||||
try {
|
||||
const response = await this.api.post(`/project/${projectId}`, projectData);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to update project: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a project
|
||||
*/
|
||||
async deleteProject(projectId: string): Promise<void> {
|
||||
try {
|
||||
await this.api.delete(`/project/${projectId}`);
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to delete project: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Convenience Methods
|
||||
|
||||
/**
|
||||
* Get today's tasks
|
||||
*/
|
||||
async getTodayTasks(): Promise<Task[]> {
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
return this.getTasks({
|
||||
startDate: today,
|
||||
endDate: today,
|
||||
completed: false,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get overdue tasks
|
||||
*/
|
||||
async getOverdueTasks(): Promise<Task[]> {
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
return this.getTasks({
|
||||
endDate: today,
|
||||
completed: false,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Search tasks by title or content
|
||||
*/
|
||||
async searchTasks(query: string, limit?: number): Promise<Task[]> {
|
||||
try {
|
||||
const params = new URLSearchParams();
|
||||
params.append('q', query);
|
||||
if (limit) params.append('limit', limit.toString());
|
||||
|
||||
const response = await this.api.get(`/task/search?${params.toString()}`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to search tasks: ${error}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
132
src/auth/ticktick-auth.ts
Normal file
132
src/auth/ticktick-auth.ts
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
import axios from 'axios';
|
||||
import { TickTickConfig, AuthTokens } from '../types/ticktick.js';
|
||||
|
||||
export class TickTickAuth {
|
||||
private config: TickTickConfig;
|
||||
private baseURL = 'https://ticktick.com';
|
||||
|
||||
constructor(config: TickTickConfig) {
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get authorization URL for OAuth flow
|
||||
*/
|
||||
getAuthorizationUrl(state?: string): string {
|
||||
const params = new URLSearchParams({
|
||||
client_id: this.config.clientId,
|
||||
redirect_uri: this.config.redirectUri,
|
||||
response_type: 'code',
|
||||
scope: 'tasks:read tasks:write',
|
||||
});
|
||||
|
||||
if (state) {
|
||||
params.append('state', state);
|
||||
}
|
||||
|
||||
return `${this.baseURL}/oauth/authorize?${params.toString()}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Exchange authorization code for access token
|
||||
*/
|
||||
async exchangeCodeForToken(code: string): Promise<AuthTokens> {
|
||||
try {
|
||||
const response = await axios.post(
|
||||
`${this.baseURL}/oauth/token`,
|
||||
{
|
||||
client_id: this.config.clientId,
|
||||
client_secret: this.config.clientSecret,
|
||||
redirect_uri: this.config.redirectUri,
|
||||
grant_type: 'authorization_code',
|
||||
code,
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const tokens: AuthTokens = response.data;
|
||||
this.config.accessToken = tokens.access_token;
|
||||
this.config.refreshToken = tokens.refresh_token;
|
||||
|
||||
return tokens;
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to exchange code for token: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh access token using refresh token
|
||||
*/
|
||||
async refreshAccessToken(): Promise<AuthTokens> {
|
||||
if (!this.config.refreshToken) {
|
||||
throw new Error('No refresh token available');
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await axios.post(
|
||||
`${this.baseURL}/oauth/token`,
|
||||
{
|
||||
client_id: this.config.clientId,
|
||||
client_secret: this.config.clientSecret,
|
||||
grant_type: 'refresh_token',
|
||||
refresh_token: this.config.refreshToken,
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const tokens: AuthTokens = response.data;
|
||||
this.config.accessToken = tokens.access_token;
|
||||
this.config.refreshToken = tokens.refresh_token;
|
||||
|
||||
return tokens;
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to refresh token: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current access token
|
||||
*/
|
||||
getAccessToken(): string | undefined {
|
||||
return this.config.accessToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set access token manually
|
||||
*/
|
||||
setAccessToken(accessToken: string, refreshToken?: string): void {
|
||||
this.config.accessToken = accessToken;
|
||||
if (refreshToken) {
|
||||
this.config.refreshToken = refreshToken;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user is authenticated
|
||||
*/
|
||||
isAuthenticated(): boolean {
|
||||
return !!this.config.accessToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get authorization headers for API requests
|
||||
*/
|
||||
getAuthHeaders(): Record<string, string> {
|
||||
if (!this.config.accessToken) {
|
||||
throw new Error('No access token available. Please authenticate first.');
|
||||
}
|
||||
|
||||
return {
|
||||
Authorization: `Bearer ${this.config.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
}
|
||||
}
|
||||
245
src/config/config-manager.ts
Normal file
245
src/config/config-manager.ts
Normal file
|
|
@ -0,0 +1,245 @@
|
|||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
import { TickTickConfig } from '../types/ticktick.js';
|
||||
import { InteractiveSetup, SetupConfig } from '../setup/interactive-setup.js';
|
||||
|
||||
export class ConfigManager {
|
||||
private static instance: ConfigManager;
|
||||
private configPath: string;
|
||||
private config: TickTickConfig | null = null;
|
||||
|
||||
private constructor() {
|
||||
const configDir = os.homedir();
|
||||
this.configPath = path.join(configDir, '.ticktick-mcp', 'config.json');
|
||||
}
|
||||
|
||||
public static getInstance(): ConfigManager {
|
||||
if (!ConfigManager.instance) {
|
||||
ConfigManager.instance = new ConfigManager();
|
||||
}
|
||||
return ConfigManager.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load configuration from multiple sources in priority order:
|
||||
* 1. Environment variables
|
||||
* 2. Saved config file
|
||||
* 3. Demo mode config
|
||||
*/
|
||||
public loadConfig(): TickTickConfig | null {
|
||||
// Check for demo mode first
|
||||
if (this.isDemoMode()) {
|
||||
return this.getDemoConfig();
|
||||
}
|
||||
|
||||
// Try environment variables first
|
||||
const envConfig = this.loadFromEnvironment();
|
||||
if (envConfig && this.isValidConfig(envConfig)) {
|
||||
this.config = envConfig;
|
||||
return envConfig;
|
||||
}
|
||||
|
||||
// Try saved config file
|
||||
const fileConfig = this.loadFromFile();
|
||||
if (fileConfig && this.isValidConfig(fileConfig)) {
|
||||
this.config = fileConfig;
|
||||
return fileConfig;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private isDemoMode(): boolean {
|
||||
return process.env.TICKTICK_DEMO_MODE === 'true' ||
|
||||
process.argv.includes('--demo') ||
|
||||
process.argv.includes('--demo-mode');
|
||||
}
|
||||
|
||||
private getDemoConfig(): TickTickConfig {
|
||||
return {
|
||||
clientId: 'demo-client-id',
|
||||
clientSecret: 'demo-client-secret',
|
||||
redirectUri: 'http://localhost:3000/callback',
|
||||
accessToken: 'demo-access-token',
|
||||
refreshToken: 'demo-refresh-token',
|
||||
};
|
||||
}
|
||||
|
||||
private loadFromEnvironment(): TickTickConfig | null {
|
||||
const clientId = process.env.TICKTICK_CLIENT_ID;
|
||||
const clientSecret = process.env.TICKTICK_CLIENT_SECRET;
|
||||
|
||||
if (!clientId || !clientSecret) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
clientId,
|
||||
clientSecret,
|
||||
redirectUri: process.env.TICKTICK_REDIRECT_URI || 'http://localhost:3000/callback',
|
||||
accessToken: process.env.TICKTICK_ACCESS_TOKEN,
|
||||
refreshToken: process.env.TICKTICK_REFRESH_TOKEN,
|
||||
};
|
||||
}
|
||||
|
||||
private loadFromFile(): TickTickConfig | null {
|
||||
const savedConfig = InteractiveSetup.loadConfig();
|
||||
if (!savedConfig) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
clientId: savedConfig.clientId,
|
||||
clientSecret: savedConfig.clientSecret,
|
||||
redirectUri: savedConfig.redirectUri,
|
||||
accessToken: savedConfig.accessToken,
|
||||
refreshToken: savedConfig.refreshToken,
|
||||
};
|
||||
}
|
||||
|
||||
private isValidConfig(config: TickTickConfig): boolean {
|
||||
return !!(config.clientId && config.clientSecret);
|
||||
}
|
||||
|
||||
public hasValidConfig(): boolean {
|
||||
const config = this.loadConfig();
|
||||
return config !== null && this.isValidConfig(config);
|
||||
}
|
||||
|
||||
public hasAuthentication(): boolean {
|
||||
const config = this.loadConfig();
|
||||
return config !== null && !!(config.accessToken && config.refreshToken);
|
||||
}
|
||||
|
||||
public getConfig(): TickTickConfig | null {
|
||||
if (!this.config) {
|
||||
this.config = this.loadConfig();
|
||||
}
|
||||
return this.config;
|
||||
}
|
||||
|
||||
public async saveTokens(accessToken: string, refreshToken: string): Promise<void> {
|
||||
const currentConfig = this.loadConfig();
|
||||
if (!currentConfig) {
|
||||
throw new Error('設定が見つかりません。まずセットアップを実行してください。');
|
||||
}
|
||||
|
||||
const updatedConfig: SetupConfig = {
|
||||
clientId: currentConfig.clientId,
|
||||
clientSecret: currentConfig.clientSecret,
|
||||
redirectUri: currentConfig.redirectUri,
|
||||
accessToken,
|
||||
refreshToken
|
||||
};
|
||||
|
||||
// Save to file
|
||||
const configDir = path.dirname(this.configPath);
|
||||
if (!fs.existsSync(configDir)) {
|
||||
fs.mkdirSync(configDir, { recursive: true });
|
||||
}
|
||||
|
||||
fs.writeFileSync(this.configPath, JSON.stringify(updatedConfig, null, 2));
|
||||
fs.chmodSync(this.configPath, 0o600); // Read/write for owner only
|
||||
|
||||
// Update in-memory config
|
||||
this.config = {
|
||||
...currentConfig,
|
||||
accessToken,
|
||||
refreshToken
|
||||
};
|
||||
}
|
||||
|
||||
public getAuthenticationStatus(): {
|
||||
hasConfig: boolean;
|
||||
hasAuth: boolean;
|
||||
isDemoMode: boolean;
|
||||
configSource: 'environment' | 'file' | 'demo' | 'none';
|
||||
} {
|
||||
const isDemoMode = this.isDemoMode();
|
||||
const hasConfig = this.hasValidConfig();
|
||||
const hasAuth = this.hasAuthentication();
|
||||
|
||||
let configSource: 'environment' | 'file' | 'demo' | 'none' = 'none';
|
||||
|
||||
if (isDemoMode) {
|
||||
configSource = 'demo';
|
||||
} else if (process.env.TICKTICK_CLIENT_ID) {
|
||||
configSource = 'environment';
|
||||
} else if (InteractiveSetup.hasConfig()) {
|
||||
configSource = 'file';
|
||||
}
|
||||
|
||||
return {
|
||||
hasConfig,
|
||||
hasAuth,
|
||||
isDemoMode,
|
||||
configSource
|
||||
};
|
||||
}
|
||||
|
||||
public displayAuthenticationStatus(): void {
|
||||
const status = this.getAuthenticationStatus();
|
||||
|
||||
if (status.isDemoMode) {
|
||||
console.error('🎭 TickTick MCP Server - デモモード');
|
||||
console.error('📊 モックデータを使用(実際のTickTickデータには接続されません)');
|
||||
console.error('');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!status.hasConfig) {
|
||||
console.error('❌ TickTick設定が見つかりません!');
|
||||
console.error('');
|
||||
console.error('設定を行うには以下のいずれかを実行してください:');
|
||||
console.error('');
|
||||
console.error('1. 対話式セットアップ(推奨):');
|
||||
console.error(' npx @ticktick-ecosystem/mcp-server --setup');
|
||||
console.error('');
|
||||
console.error('2. 環境変数設定:');
|
||||
console.error(' export TICKTICK_CLIENT_ID="your_client_id"');
|
||||
console.error(' export TICKTICK_CLIENT_SECRET="your_client_secret"');
|
||||
console.error('');
|
||||
console.error('3. デモモードでテスト:');
|
||||
console.error(' npx @ticktick-ecosystem/mcp-server --demo');
|
||||
console.error('');
|
||||
return;
|
||||
}
|
||||
|
||||
console.error('🚀 TickTick MCP Server起動中...');
|
||||
console.error(`📁 設定ソース: ${this.getConfigSourceName(status.configSource)}`);
|
||||
|
||||
if (status.hasAuth) {
|
||||
console.error('✅ TickTick認証: 完了');
|
||||
} else {
|
||||
console.error('⚠️ TickTick認証: 必要');
|
||||
console.error(' OAuth認証を完了してください');
|
||||
}
|
||||
|
||||
console.error('📡 MCP Server listening on stdio...');
|
||||
console.error('');
|
||||
}
|
||||
|
||||
private getConfigSourceName(source: string): string {
|
||||
switch (source) {
|
||||
case 'environment': return '環境変数';
|
||||
case 'file': return '設定ファイル';
|
||||
case 'demo': return 'デモモード';
|
||||
default: return '不明';
|
||||
}
|
||||
}
|
||||
|
||||
public getSetupInstructions(): void {
|
||||
console.error('🔧 TickTick MCP Server セットアップ');
|
||||
console.error('====================================');
|
||||
console.error('');
|
||||
console.error('次のコマンドで対話式セットアップを開始してください:');
|
||||
console.error('');
|
||||
console.error(' npx @ticktick-ecosystem/mcp-server --setup');
|
||||
console.error('');
|
||||
console.error('または、今すぐデモモードでテストしてみてください:');
|
||||
console.error('');
|
||||
console.error(' npx @ticktick-ecosystem/mcp-server --demo');
|
||||
console.error('');
|
||||
}
|
||||
}
|
||||
275
src/demo/mock-data.ts
Normal file
275
src/demo/mock-data.ts
Normal file
|
|
@ -0,0 +1,275 @@
|
|||
// Mock data for demo mode
|
||||
import { Task, Project } from '../types/ticktick.js';
|
||||
import { ITickTickAPI } from '../types/api-interface.js';
|
||||
|
||||
export const mockTasks: Task[] = [
|
||||
{
|
||||
id: 'demo-task-1',
|
||||
title: 'Complete MCP Server Documentation',
|
||||
content: 'Write comprehensive documentation for the TickTick MCP server',
|
||||
dueDate: new Date(Date.now() + 86400000).toISOString(), // Tomorrow
|
||||
priority: 3,
|
||||
projectId: 'demo-project-1',
|
||||
status: 0,
|
||||
createdTime: new Date().toISOString(),
|
||||
modifiedTime: new Date().toISOString(),
|
||||
},
|
||||
{
|
||||
id: 'demo-task-2',
|
||||
title: 'Review Pull Requests',
|
||||
content: 'Review and merge pending pull requests',
|
||||
dueDate: new Date().toISOString(), // Today
|
||||
priority: 5,
|
||||
projectId: 'demo-project-1',
|
||||
status: 0,
|
||||
createdTime: new Date(Date.now() - 86400000).toISOString(), // Yesterday
|
||||
modifiedTime: new Date().toISOString(),
|
||||
},
|
||||
{
|
||||
id: 'demo-task-3',
|
||||
title: 'Plan Weekly Meeting',
|
||||
content: 'Prepare agenda for weekly team meeting',
|
||||
dueDate: new Date(Date.now() - 86400000).toISOString(), // Yesterday (overdue)
|
||||
priority: 1,
|
||||
projectId: 'demo-project-2',
|
||||
status: 0,
|
||||
createdTime: new Date(Date.now() - 172800000).toISOString(), // 2 days ago
|
||||
modifiedTime: new Date().toISOString(),
|
||||
},
|
||||
{
|
||||
id: 'demo-task-4',
|
||||
title: 'Update Website Content',
|
||||
content: 'Update the company website with latest product information',
|
||||
dueDate: new Date(Date.now() + 604800000).toISOString(), // Next week
|
||||
priority: 2,
|
||||
projectId: 'demo-project-2',
|
||||
status: 0,
|
||||
createdTime: new Date().toISOString(),
|
||||
modifiedTime: new Date().toISOString(),
|
||||
},
|
||||
{
|
||||
id: 'demo-task-5',
|
||||
title: 'Completed Task Example',
|
||||
content: 'This is an example of a completed task',
|
||||
dueDate: new Date(Date.now() - 86400000).toISOString(),
|
||||
priority: 3,
|
||||
projectId: 'demo-project-1',
|
||||
status: 1, // Completed
|
||||
createdTime: new Date(Date.now() - 172800000).toISOString(),
|
||||
modifiedTime: new Date().toISOString(),
|
||||
completedTime: new Date().toISOString(),
|
||||
},
|
||||
];
|
||||
|
||||
export const mockProjects: Project[] = [
|
||||
{
|
||||
id: 'demo-project-1',
|
||||
name: 'Development Tasks',
|
||||
color: '#3498db',
|
||||
sortOrder: 1,
|
||||
modifiedTime: new Date().toISOString(),
|
||||
closed: false,
|
||||
kind: 'project',
|
||||
},
|
||||
{
|
||||
id: 'demo-project-2',
|
||||
name: 'Marketing & Content',
|
||||
color: '#e74c3c',
|
||||
sortOrder: 2,
|
||||
modifiedTime: new Date().toISOString(),
|
||||
closed: false,
|
||||
kind: 'project',
|
||||
},
|
||||
{
|
||||
id: 'demo-project-3',
|
||||
name: 'Personal',
|
||||
color: '#2ecc71',
|
||||
sortOrder: 3,
|
||||
modifiedTime: new Date().toISOString(),
|
||||
closed: false,
|
||||
kind: 'project',
|
||||
},
|
||||
];
|
||||
|
||||
// Helper functions for demo mode
|
||||
export class MockTickTickAPI implements ITickTickAPI {
|
||||
private tasks: Task[] = [...mockTasks];
|
||||
private projects: Project[] = [...mockProjects];
|
||||
|
||||
async getTasks(filter: any = {}): Promise<Task[]> {
|
||||
let filteredTasks = [...this.tasks];
|
||||
|
||||
if (filter.projectId) {
|
||||
filteredTasks = filteredTasks.filter(task => task.projectId === filter.projectId);
|
||||
}
|
||||
|
||||
if (filter.completed !== undefined) {
|
||||
filteredTasks = filteredTasks.filter(task =>
|
||||
filter.completed ? task.status === 1 : task.status === 0
|
||||
);
|
||||
}
|
||||
|
||||
if (filter.startDate && filter.endDate) {
|
||||
filteredTasks = filteredTasks.filter(task => {
|
||||
if (!task.dueDate) return false;
|
||||
const taskDate = new Date(task.dueDate);
|
||||
return taskDate >= new Date(filter.startDate) && taskDate <= new Date(filter.endDate);
|
||||
});
|
||||
}
|
||||
|
||||
if (filter.limit) {
|
||||
filteredTasks = filteredTasks.slice(0, filter.limit);
|
||||
}
|
||||
|
||||
return filteredTasks;
|
||||
}
|
||||
|
||||
async getTask(taskId: string): Promise<Task> {
|
||||
const task = this.tasks.find(t => t.id === taskId);
|
||||
if (!task) {
|
||||
throw new Error(`Task not found: ${taskId}`);
|
||||
}
|
||||
return task;
|
||||
}
|
||||
|
||||
async createTask(taskData: any): Promise<Task> {
|
||||
const newTask: Task = {
|
||||
id: `demo-task-${Date.now()}`,
|
||||
title: taskData.title,
|
||||
content: taskData.content || '',
|
||||
dueDate: taskData.dueDate,
|
||||
priority: taskData.priority || 0,
|
||||
projectId: taskData.projectId || 'demo-project-1',
|
||||
status: 0,
|
||||
createdTime: new Date().toISOString(),
|
||||
modifiedTime: new Date().toISOString(),
|
||||
tags: taskData.tags || [],
|
||||
};
|
||||
|
||||
this.tasks.push(newTask);
|
||||
return newTask;
|
||||
}
|
||||
|
||||
async updateTask(taskData: any): Promise<Task> {
|
||||
const taskIndex = this.tasks.findIndex(t => t.id === taskData.id);
|
||||
if (taskIndex === -1) {
|
||||
throw new Error(`Task not found: ${taskData.id}`);
|
||||
}
|
||||
|
||||
this.tasks[taskIndex] = {
|
||||
...this.tasks[taskIndex],
|
||||
...taskData,
|
||||
modifiedTime: new Date().toISOString(),
|
||||
};
|
||||
|
||||
return this.tasks[taskIndex];
|
||||
}
|
||||
|
||||
async completeTask(taskId: string): Promise<Task> {
|
||||
const taskIndex = this.tasks.findIndex(t => t.id === taskId);
|
||||
if (taskIndex === -1) {
|
||||
throw new Error(`Task not found: ${taskId}`);
|
||||
}
|
||||
|
||||
this.tasks[taskIndex] = {
|
||||
...this.tasks[taskIndex],
|
||||
status: 1,
|
||||
completedTime: new Date().toISOString(),
|
||||
modifiedTime: new Date().toISOString(),
|
||||
};
|
||||
|
||||
return this.tasks[taskIndex];
|
||||
}
|
||||
|
||||
async deleteTask(taskId: string): Promise<void> {
|
||||
const taskIndex = this.tasks.findIndex(t => t.id === taskId);
|
||||
if (taskIndex === -1) {
|
||||
throw new Error(`Task not found: ${taskId}`);
|
||||
}
|
||||
|
||||
this.tasks.splice(taskIndex, 1);
|
||||
}
|
||||
|
||||
async getProjects(): Promise<Project[]> {
|
||||
return [...this.projects];
|
||||
}
|
||||
|
||||
async getProject(projectId: string): Promise<Project> {
|
||||
const project = this.projects.find(p => p.id === projectId);
|
||||
if (!project) {
|
||||
throw new Error(`Project not found: ${projectId}`);
|
||||
}
|
||||
return project;
|
||||
}
|
||||
|
||||
async createProject(projectData: any): Promise<Project> {
|
||||
const newProject: Project = {
|
||||
id: `demo-project-${Date.now()}`,
|
||||
name: projectData.name,
|
||||
color: projectData.color || '#3498db',
|
||||
sortOrder: this.projects.length + 1,
|
||||
modifiedTime: new Date().toISOString(),
|
||||
closed: false,
|
||||
kind: 'project',
|
||||
};
|
||||
|
||||
this.projects.push(newProject);
|
||||
return newProject;
|
||||
}
|
||||
|
||||
async updateProject(projectId: string, projectData: any): Promise<Project> {
|
||||
const projectIndex = this.projects.findIndex(p => p.id === projectId);
|
||||
if (projectIndex === -1) {
|
||||
throw new Error(`Project not found: ${projectId}`);
|
||||
}
|
||||
|
||||
this.projects[projectIndex] = {
|
||||
...this.projects[projectIndex],
|
||||
...projectData,
|
||||
modifiedTime: new Date().toISOString(),
|
||||
};
|
||||
|
||||
return this.projects[projectIndex];
|
||||
}
|
||||
|
||||
async deleteProject(projectId: string): Promise<void> {
|
||||
const projectIndex = this.projects.findIndex(p => p.id === projectId);
|
||||
if (projectIndex === -1) {
|
||||
throw new Error(`Project not found: ${projectId}`);
|
||||
}
|
||||
|
||||
this.projects.splice(projectIndex, 1);
|
||||
}
|
||||
|
||||
// Convenience methods
|
||||
async getTodayTasks(): Promise<Task[]> {
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
return this.getTasks({
|
||||
startDate: today,
|
||||
endDate: today,
|
||||
completed: false,
|
||||
});
|
||||
}
|
||||
|
||||
async getOverdueTasks(): Promise<Task[]> {
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
return this.tasks.filter(task => {
|
||||
if (!task.dueDate || task.status === 1) return false;
|
||||
return new Date(task.dueDate) < new Date(today);
|
||||
});
|
||||
}
|
||||
|
||||
async searchTasks(query: string, limit?: number): Promise<Task[]> {
|
||||
const lowercaseQuery = query.toLowerCase();
|
||||
let results = this.tasks.filter(task =>
|
||||
task.title.toLowerCase().includes(lowercaseQuery) ||
|
||||
(task.content && task.content.toLowerCase().includes(lowercaseQuery))
|
||||
);
|
||||
|
||||
if (limit) {
|
||||
results = results.slice(0, limit);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
}
|
||||
106
src/index.ts
Normal file
106
src/index.ts
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
import { TickTickMCPServer } from './server.js';
|
||||
import { TickTickConfig } from './types/ticktick.js';
|
||||
import { ConfigManager } from './config/config-manager.js';
|
||||
import { InteractiveSetup } from './setup/interactive-setup.js';
|
||||
|
||||
async function main() {
|
||||
// Check for setup command
|
||||
if (process.argv.includes('--setup') || process.argv.includes('--configure')) {
|
||||
console.error('🔧 TickTick MCP Server セットアップを開始します...\n');
|
||||
const setup = new InteractiveSetup();
|
||||
await setup.run();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for demo mode
|
||||
const isDemoMode = process.env.TICKTICK_DEMO_MODE === 'true' ||
|
||||
process.argv.includes('--demo') ||
|
||||
process.argv.includes('--demo-mode');
|
||||
|
||||
if (isDemoMode) {
|
||||
console.error('🎭 TickTick MCP Server - デモモード起動中...');
|
||||
console.error('📊 モックデータを使用(実際のTickTickデータには接続されません)');
|
||||
console.error('');
|
||||
|
||||
const demoConfig: TickTickConfig = {
|
||||
clientId: 'demo-client-id',
|
||||
clientSecret: 'demo-client-secret',
|
||||
redirectUri: 'http://localhost:3000/callback',
|
||||
accessToken: 'demo-access-token',
|
||||
refreshToken: 'demo-refresh-token',
|
||||
};
|
||||
|
||||
const server = new TickTickMCPServer(demoConfig, true); // true = demo mode
|
||||
console.error('✅ デモモード: 準備完了');
|
||||
console.error('📡 MCP Server listening on stdio...');
|
||||
console.error('');
|
||||
|
||||
await server.start();
|
||||
return;
|
||||
}
|
||||
|
||||
// Use ConfigManager to load configuration
|
||||
const configManager = ConfigManager.getInstance();
|
||||
const config = configManager.loadConfig();
|
||||
|
||||
if (!config) {
|
||||
console.error('❌ TickTick設定が見つかりません!');
|
||||
console.error('');
|
||||
console.error('設定を行うには以下のいずれかを実行してください:');
|
||||
console.error('');
|
||||
console.error('1. 対話式セットアップ(推奨):');
|
||||
console.error(' npx @ticktick-ecosystem/mcp-server --setup');
|
||||
console.error('');
|
||||
console.error('2. 環境変数設定:');
|
||||
console.error(' export TICKTICK_CLIENT_ID="your_client_id"');
|
||||
console.error(' export TICKTICK_CLIENT_SECRET="your_client_secret"');
|
||||
console.error('');
|
||||
console.error('3. デモモードでテスト:');
|
||||
console.error(' npx @ticktick-ecosystem/mcp-server --demo');
|
||||
console.error('');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
try {
|
||||
// Display configuration status
|
||||
configManager.displayAuthenticationStatus();
|
||||
|
||||
const server = new TickTickMCPServer(config);
|
||||
|
||||
await server.start();
|
||||
} catch (error) {
|
||||
console.error('❌ TickTick MCP Server起動エラー:', error);
|
||||
console.error('');
|
||||
console.error('エラーが発生しました。以下を確認してください:');
|
||||
console.error('- 設定ファイルの内容');
|
||||
console.error('- ネットワーク接続');
|
||||
console.error('- TickTick API認証情報');
|
||||
console.error('');
|
||||
console.error('再セットアップが必要な場合:');
|
||||
console.error(' npx @ticktick-ecosystem/mcp-server --setup');
|
||||
console.error('');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle graceful shutdown
|
||||
process.on('SIGINT', () => {
|
||||
console.error('\n🛑 TickTick MCP Server を終了しています...');
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
process.on('SIGTERM', () => {
|
||||
console.error('\n🛑 TickTick MCP Server を終了しています...');
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
main().catch((error) => {
|
||||
console.error('💥 未処理エラー:', error);
|
||||
console.error('');
|
||||
console.error('問題が継続する場合は、セットアップを再実行してください:');
|
||||
console.error(' npx @ticktick-ecosystem/mcp-server --setup');
|
||||
console.error('');
|
||||
process.exit(1);
|
||||
});
|
||||
281
src/prompts/planning-prompts.ts
Normal file
281
src/prompts/planning-prompts.ts
Normal file
|
|
@ -0,0 +1,281 @@
|
|||
import { Prompt } from '@modelcontextprotocol/sdk/types.js';
|
||||
import { ITickTickAPI } from '../types/api-interface.js';
|
||||
|
||||
export class PlanningPrompts {
|
||||
constructor(private api: ITickTickAPI) {}
|
||||
|
||||
getPrompts(): Prompt[] {
|
||||
return [
|
||||
{
|
||||
name: 'daily_planning',
|
||||
description: 'Help plan your daily tasks and priorities',
|
||||
arguments: [
|
||||
{
|
||||
name: 'focus_areas',
|
||||
description: 'Specific areas or projects to focus on today',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: 'available_hours',
|
||||
description: 'Number of available working hours today',
|
||||
required: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'task_breakdown',
|
||||
description: 'Break down a complex task into smaller, manageable subtasks',
|
||||
arguments: [
|
||||
{
|
||||
name: 'main_task',
|
||||
description: 'The main task or project to break down',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'deadline',
|
||||
description: 'Deadline for the main task',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: 'complexity',
|
||||
description: 'Complexity level (simple, medium, complex)',
|
||||
required: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'priority_analysis',
|
||||
description: 'Analyze and suggest priorities for your current tasks',
|
||||
arguments: [
|
||||
{
|
||||
name: 'project_filter',
|
||||
description: 'Filter analysis to specific project',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: 'time_frame',
|
||||
description: 'Time frame for analysis (today, week, month)',
|
||||
required: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'weekly_review',
|
||||
description: 'Review your productivity and plan for the upcoming week',
|
||||
arguments: [
|
||||
{
|
||||
name: 'goals',
|
||||
description: 'Specific goals for the upcoming week',
|
||||
required: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'project_planning',
|
||||
description: 'Create a comprehensive plan for a new project',
|
||||
arguments: [
|
||||
{
|
||||
name: 'project_name',
|
||||
description: 'Name of the new project',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'project_scope',
|
||||
description: 'Scope and objectives of the project',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'timeline',
|
||||
description: 'Expected timeline for the project',
|
||||
required: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
async getPromptContent(name: string, args: Record<string, string> = {}): Promise<string> {
|
||||
try {
|
||||
switch (name) {
|
||||
case 'daily_planning': {
|
||||
const todayTasks = await this.api.getTodayTasks();
|
||||
const overdueTasks = await this.api.getOverdueTasks();
|
||||
|
||||
let prompt = `# Daily Planning Assistant
|
||||
|
||||
## Today's Current Tasks (${todayTasks.length})
|
||||
${todayTasks.map(task => `- ${task.title}${task.priority ? ` (Priority: ${task.priority})` : ''}`).join('\n')}
|
||||
|
||||
## Overdue Tasks (${overdueTasks.length})
|
||||
${overdueTasks.map(task => `- ${task.title} (Due: ${task.dueDate})`).join('\n')}
|
||||
|
||||
## Planning Guidance
|
||||
Based on your current tasks, please help me:
|
||||
1. Prioritize today's tasks effectively
|
||||
2. Allocate time for each priority task
|
||||
3. Handle overdue items appropriately
|
||||
4. Maintain a healthy work-life balance
|
||||
|
||||
`;
|
||||
|
||||
if (args.focus_areas) {
|
||||
prompt += `## Focus Areas\nToday I want to focus on: ${args.focus_areas}\n\n`;
|
||||
}
|
||||
|
||||
if (args.available_hours) {
|
||||
prompt += `## Available Time\nI have ${args.available_hours} hours available for work today.\n\n`;
|
||||
}
|
||||
|
||||
prompt += `Please provide a structured daily plan with time allocations and priority recommendations.`;
|
||||
|
||||
return prompt;
|
||||
}
|
||||
|
||||
case 'task_breakdown': {
|
||||
const mainTask = args.main_task;
|
||||
const deadline = args.deadline || 'No specific deadline';
|
||||
const complexity = args.complexity || 'medium';
|
||||
|
||||
return `# Task Breakdown Assistant
|
||||
|
||||
## Main Task
|
||||
**Task:** ${mainTask}
|
||||
**Deadline:** ${deadline}
|
||||
**Complexity Level:** ${complexity}
|
||||
|
||||
## Breakdown Request
|
||||
Please help me break down this task into smaller, actionable subtasks that are:
|
||||
1. Specific and measurable
|
||||
2. Appropriately sized (2-4 hours each)
|
||||
3. Logically sequenced
|
||||
4. Realistic given the deadline
|
||||
|
||||
## Output Format
|
||||
Please provide:
|
||||
- [ ] Subtask 1 (estimated time)
|
||||
- [ ] Subtask 2 (estimated time)
|
||||
- [ ] Subtask 3 (estimated time)
|
||||
...
|
||||
|
||||
Include dependencies between tasks and suggest which subtasks could be done in parallel.`;
|
||||
}
|
||||
|
||||
case 'priority_analysis': {
|
||||
const incompleteTasks = await this.api.getTasks({ completed: false });
|
||||
const timeFrame = args.time_frame || 'week';
|
||||
|
||||
let filteredTasks = incompleteTasks;
|
||||
if (args.project_filter) {
|
||||
filteredTasks = incompleteTasks.filter(task =>
|
||||
task.projectId === args.project_filter
|
||||
);
|
||||
}
|
||||
|
||||
return `# Priority Analysis Assistant
|
||||
|
||||
## Current Tasks for Analysis (${filteredTasks.length})
|
||||
${filteredTasks.map(task =>
|
||||
`- ${task.title}${task.dueDate ? ` (Due: ${task.dueDate})` : ''}${task.priority ? ` (Priority: ${task.priority})` : ''}`
|
||||
).join('\n')}
|
||||
|
||||
## Analysis Request
|
||||
Please analyze these tasks for the ${timeFrame} and provide:
|
||||
|
||||
1. **High Priority** - Tasks that should be done first
|
||||
2. **Medium Priority** - Tasks that are important but not urgent
|
||||
3. **Low Priority** - Tasks that can be deferred if needed
|
||||
|
||||
## Criteria for Analysis
|
||||
Consider:
|
||||
- Due dates and deadlines
|
||||
- Task complexity and time requirements
|
||||
- Dependencies between tasks
|
||||
- Impact on other projects or people
|
||||
- Personal/professional goals
|
||||
|
||||
Please provide specific recommendations for task sequencing and time management.`;
|
||||
}
|
||||
|
||||
case 'weekly_review': {
|
||||
const completedTasks = await this.api.getTasks({ completed: true });
|
||||
const incompleteTasks = await this.api.getTasks({ completed: false });
|
||||
|
||||
// Filter for tasks from the past week
|
||||
const weekAgo = new Date();
|
||||
weekAgo.setDate(weekAgo.getDate() - 7);
|
||||
|
||||
const recentCompleted = completedTasks.filter(task =>
|
||||
task.completedTime && new Date(task.completedTime) >= weekAgo
|
||||
);
|
||||
|
||||
return `# Weekly Review Assistant
|
||||
|
||||
## This Week's Accomplishments (${recentCompleted.length})
|
||||
${recentCompleted.map(task => `- ✅ ${task.title}`).join('\n')}
|
||||
|
||||
## Pending Tasks (${incompleteTasks.length})
|
||||
${incompleteTasks.slice(0, 10).map(task => `- ⏳ ${task.title}`).join('\n')}
|
||||
${incompleteTasks.length > 10 ? `... and ${incompleteTasks.length - 10} more` : ''}
|
||||
|
||||
## Review Questions
|
||||
Please help me reflect on:
|
||||
1. What went well this week?
|
||||
2. What challenges did I face?
|
||||
3. How can I improve my productivity next week?
|
||||
4. Which pending tasks should be prioritized?
|
||||
|
||||
${args.goals ? `## Goals for Next Week\n${args.goals}\n\n` : ''}
|
||||
|
||||
Please provide a comprehensive weekly review with actionable insights for improvement.`;
|
||||
}
|
||||
|
||||
case 'project_planning': {
|
||||
const projects = await this.api.getProjects();
|
||||
const projectName = args.project_name;
|
||||
const projectScope = args.project_scope;
|
||||
const timeline = args.timeline || 'To be determined';
|
||||
|
||||
return `# Project Planning Assistant
|
||||
|
||||
## New Project Details
|
||||
**Project Name:** ${projectName}
|
||||
**Scope:** ${projectScope}
|
||||
**Timeline:** ${timeline}
|
||||
|
||||
## Existing Projects (${projects.length})
|
||||
${projects.map(project => `- ${project.name}`).join('\n')}
|
||||
|
||||
## Planning Request
|
||||
Please help me create a comprehensive project plan including:
|
||||
|
||||
1. **Project Breakdown Structure**
|
||||
- Major phases or milestones
|
||||
- Key deliverables
|
||||
- Dependencies and risks
|
||||
|
||||
2. **Task Planning**
|
||||
- Specific actionable tasks
|
||||
- Time estimates
|
||||
- Resource requirements
|
||||
|
||||
3. **Timeline and Scheduling**
|
||||
- Project phases with dates
|
||||
- Critical path identification
|
||||
- Buffer time for uncertainties
|
||||
|
||||
4. **Integration Considerations**
|
||||
- How this project fits with existing projects
|
||||
- Resource allocation
|
||||
- Potential conflicts or synergies
|
||||
|
||||
Please provide a structured project plan that I can implement in TickTick.`;
|
||||
}
|
||||
|
||||
default:
|
||||
throw new Error(`Unknown prompt: ${name}`);
|
||||
}
|
||||
} catch (error) {
|
||||
return `Error generating prompt: ${error instanceof Error ? error.message : String(error)}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
188
src/resources/task-resources.ts
Normal file
188
src/resources/task-resources.ts
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
import { Resource } from '@modelcontextprotocol/sdk/types.js';
|
||||
import { ITickTickAPI } from '../types/api-interface.js';
|
||||
|
||||
export class TaskResources {
|
||||
constructor(private api: ITickTickAPI) {}
|
||||
|
||||
getResources(): Resource[] {
|
||||
return [
|
||||
{
|
||||
uri: 'ticktick://tasks/today',
|
||||
name: "Today's Tasks",
|
||||
description: 'All tasks scheduled for today',
|
||||
mimeType: 'application/json',
|
||||
},
|
||||
{
|
||||
uri: 'ticktick://tasks/overdue',
|
||||
name: 'Overdue Tasks',
|
||||
description: 'All tasks that are past their due date',
|
||||
mimeType: 'application/json',
|
||||
},
|
||||
{
|
||||
uri: 'ticktick://tasks/upcoming',
|
||||
name: 'Upcoming Tasks',
|
||||
description: 'Tasks scheduled for the next 7 days',
|
||||
mimeType: 'application/json',
|
||||
},
|
||||
{
|
||||
uri: 'ticktick://tasks/all',
|
||||
name: 'All Tasks',
|
||||
description: 'All tasks in the account',
|
||||
mimeType: 'application/json',
|
||||
},
|
||||
{
|
||||
uri: 'ticktick://tasks/completed',
|
||||
name: 'Completed Tasks',
|
||||
description: 'All completed tasks',
|
||||
mimeType: 'application/json',
|
||||
},
|
||||
{
|
||||
uri: 'ticktick://tasks/incomplete',
|
||||
name: 'Incomplete Tasks',
|
||||
description: 'All incomplete tasks',
|
||||
mimeType: 'application/json',
|
||||
},
|
||||
{
|
||||
uri: 'ticktick://projects/all',
|
||||
name: 'All Projects',
|
||||
description: 'All projects in the account',
|
||||
mimeType: 'application/json',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
async getResourceContent(uri: string): Promise<any> {
|
||||
try {
|
||||
switch (uri) {
|
||||
case 'ticktick://tasks/today': {
|
||||
const tasks = await this.api.getTodayTasks();
|
||||
return {
|
||||
success: true,
|
||||
data: tasks,
|
||||
metadata: {
|
||||
count: tasks.length,
|
||||
date: new Date().toISOString().split('T')[0],
|
||||
type: 'today_tasks',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
case 'ticktick://tasks/overdue': {
|
||||
const tasks = await this.api.getOverdueTasks();
|
||||
return {
|
||||
success: true,
|
||||
data: tasks,
|
||||
metadata: {
|
||||
count: tasks.length,
|
||||
type: 'overdue_tasks',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
case 'ticktick://tasks/upcoming': {
|
||||
const today = new Date();
|
||||
const nextWeek = new Date(today);
|
||||
nextWeek.setDate(today.getDate() + 7);
|
||||
|
||||
const tasks = await this.api.getTasks({
|
||||
startDate: today.toISOString().split('T')[0],
|
||||
endDate: nextWeek.toISOString().split('T')[0],
|
||||
completed: false,
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: tasks,
|
||||
metadata: {
|
||||
count: tasks.length,
|
||||
startDate: today.toISOString().split('T')[0],
|
||||
endDate: nextWeek.toISOString().split('T')[0],
|
||||
type: 'upcoming_tasks',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
case 'ticktick://tasks/all': {
|
||||
const tasks = await this.api.getTasks();
|
||||
return {
|
||||
success: true,
|
||||
data: tasks,
|
||||
metadata: {
|
||||
count: tasks.length,
|
||||
type: 'all_tasks',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
case 'ticktick://tasks/completed': {
|
||||
const tasks = await this.api.getTasks({ completed: true });
|
||||
return {
|
||||
success: true,
|
||||
data: tasks,
|
||||
metadata: {
|
||||
count: tasks.length,
|
||||
type: 'completed_tasks',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
case 'ticktick://tasks/incomplete': {
|
||||
const tasks = await this.api.getTasks({ completed: false });
|
||||
return {
|
||||
success: true,
|
||||
data: tasks,
|
||||
metadata: {
|
||||
count: tasks.length,
|
||||
type: 'incomplete_tasks',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
case 'ticktick://projects/all': {
|
||||
const projects = await this.api.getProjects();
|
||||
return {
|
||||
success: true,
|
||||
data: projects,
|
||||
metadata: {
|
||||
count: projects.length,
|
||||
type: 'all_projects',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
default:
|
||||
// Handle dynamic URIs like ticktick://projects/{projectId}/tasks
|
||||
if (uri.startsWith('ticktick://projects/') && uri.endsWith('/tasks')) {
|
||||
const projectId = uri.split('/')[2];
|
||||
const tasks = await this.api.getTasks({ projectId });
|
||||
return {
|
||||
success: true,
|
||||
data: tasks,
|
||||
metadata: {
|
||||
count: tasks.length,
|
||||
projectId,
|
||||
type: 'project_tasks',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
throw new Error(`Unknown resource URI: ${uri}`);
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Helper method to create dynamic project task resources
|
||||
createProjectTaskResource(projectId: string, projectName: string): Resource {
|
||||
return {
|
||||
uri: `ticktick://projects/${projectId}/tasks`,
|
||||
name: `${projectName} Tasks`,
|
||||
description: `All tasks in the ${projectName} project`,
|
||||
mimeType: 'application/json',
|
||||
};
|
||||
}
|
||||
}
|
||||
261
src/server.ts
Normal file
261
src/server.ts
Normal file
|
|
@ -0,0 +1,261 @@
|
|||
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
||||
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
||||
import {
|
||||
CallToolRequestSchema,
|
||||
ListPromptsRequestSchema,
|
||||
GetPromptRequestSchema,
|
||||
ListResourcesRequestSchema,
|
||||
ReadResourceRequestSchema,
|
||||
ListToolsRequestSchema,
|
||||
} from '@modelcontextprotocol/sdk/types.js';
|
||||
|
||||
import { TickTickAuth } from './auth/ticktick-auth.js';
|
||||
import { TickTickAPI } from './auth/ticktick-api.js';
|
||||
import { TaskTools } from './tools/task-tools.js';
|
||||
import { ProjectTools } from './tools/project-tools.js';
|
||||
import { TaskResources } from './resources/task-resources.js';
|
||||
import { PlanningPrompts } from './prompts/planning-prompts.js';
|
||||
import { TickTickConfig } from './types/ticktick.js';
|
||||
import { MockTickTickAPI } from './demo/mock-data.js';
|
||||
import { ConfigManager } from './config/config-manager.js';
|
||||
import { ITickTickAPI } from './types/api-interface.js';
|
||||
|
||||
export class TickTickMCPServer {
|
||||
private server: Server;
|
||||
private auth: TickTickAuth;
|
||||
private api: ITickTickAPI;
|
||||
private taskTools: TaskTools;
|
||||
private projectTools: ProjectTools;
|
||||
private taskResources: TaskResources;
|
||||
private planningPrompts: PlanningPrompts;
|
||||
private isDemoMode: boolean;
|
||||
private configManager: ConfigManager;
|
||||
|
||||
constructor(config: TickTickConfig, demoMode: boolean = false) {
|
||||
this.isDemoMode = demoMode;
|
||||
this.configManager = ConfigManager.getInstance();
|
||||
this.server = new Server({
|
||||
name: '@ticktick-ecosystem/mcp-server',
|
||||
version: '1.0.0',
|
||||
});
|
||||
|
||||
// Initialize TickTick components
|
||||
this.auth = new TickTickAuth(config);
|
||||
|
||||
if (this.isDemoMode) {
|
||||
this.api = new MockTickTickAPI();
|
||||
} else {
|
||||
this.api = new TickTickAPI(this.auth);
|
||||
}
|
||||
|
||||
this.taskTools = new TaskTools(this.api);
|
||||
this.projectTools = new ProjectTools(this.api);
|
||||
this.taskResources = new TaskResources(this.api);
|
||||
this.planningPrompts = new PlanningPrompts(this.api);
|
||||
|
||||
this.setupHandlers();
|
||||
}
|
||||
|
||||
private setupHandlers(): void {
|
||||
// Tools handlers
|
||||
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||
const taskTools = this.taskTools.getTools();
|
||||
const projectTools = this.projectTools.getTools();
|
||||
|
||||
return {
|
||||
tools: [...taskTools, ...projectTools],
|
||||
};
|
||||
});
|
||||
|
||||
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
const { name, arguments: args } = request.params;
|
||||
|
||||
try {
|
||||
// Check if user is authenticated (skip for demo mode)
|
||||
if (!this.isDemoMode && !this.auth.isAuthenticated()) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify({
|
||||
success: false,
|
||||
error: '認証が必要です。セットアップを実行してください。',
|
||||
setup_command: 'npx @ticktick-ecosystem/mcp-server --setup',
|
||||
authUrl: this.auth.getAuthorizationUrl(),
|
||||
}),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
// Route to appropriate tool handler
|
||||
let result;
|
||||
const taskToolNames = this.taskTools.getTools().map(t => t.name);
|
||||
const projectToolNames = this.projectTools.getTools().map(t => t.name);
|
||||
|
||||
if (taskToolNames.includes(name)) {
|
||||
result = await this.taskTools.handleToolCall(name, args || {});
|
||||
} else if (projectToolNames.includes(name)) {
|
||||
result = await this.projectTools.handleToolCall(name, args || {});
|
||||
} else {
|
||||
result = {
|
||||
success: false,
|
||||
error: `Unknown tool: ${name}`,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
}, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// Resources handlers
|
||||
this.server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
||||
return {
|
||||
resources: this.taskResources.getResources(),
|
||||
};
|
||||
});
|
||||
|
||||
this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
||||
const { uri } = request.params;
|
||||
|
||||
try {
|
||||
if (!this.isDemoMode && !this.auth.isAuthenticated()) {
|
||||
return {
|
||||
contents: [
|
||||
{
|
||||
uri,
|
||||
mimeType: 'application/json',
|
||||
text: JSON.stringify({
|
||||
success: false,
|
||||
error: '認証が必要です。セットアップを実行してください。',
|
||||
setup_command: 'npx @ticktick-ecosystem/mcp-server --setup',
|
||||
}),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
const result = await this.taskResources.getResourceContent(uri);
|
||||
return {
|
||||
contents: [
|
||||
{
|
||||
uri,
|
||||
mimeType: 'application/json',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
contents: [
|
||||
{
|
||||
uri,
|
||||
mimeType: 'application/json',
|
||||
text: JSON.stringify({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
}),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// Prompts handlers
|
||||
this.server.setRequestHandler(ListPromptsRequestSchema, async () => {
|
||||
return {
|
||||
prompts: this.planningPrompts.getPrompts(),
|
||||
};
|
||||
});
|
||||
|
||||
this.server.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
||||
const { name, arguments: args } = request.params;
|
||||
|
||||
try {
|
||||
if (!this.isDemoMode && !this.auth.isAuthenticated()) {
|
||||
return {
|
||||
description: '認証が必要です',
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
content: {
|
||||
type: 'text',
|
||||
text: '認証が必要です。セットアップを実行してください: npx @ticktick-ecosystem/mcp-server --setup',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
const promptContent = await this.planningPrompts.getPromptContent(name, args || {});
|
||||
return {
|
||||
description: `TickTick ${name} prompt`,
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
content: {
|
||||
type: 'text',
|
||||
text: promptContent,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
description: 'Error generating prompt',
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
content: {
|
||||
type: 'text',
|
||||
text: `Error: ${error instanceof Error ? error.message : String(error)}`,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async start(): Promise<void> {
|
||||
// Use the built-in stdio transport
|
||||
const transport = new StdioServerTransport();
|
||||
await this.server.connect(transport);
|
||||
}
|
||||
|
||||
// Helper methods for authentication
|
||||
getAuthorizationUrl(): string {
|
||||
return this.auth.getAuthorizationUrl();
|
||||
}
|
||||
|
||||
async setAuthorizationCode(code: string): Promise<void> {
|
||||
await this.auth.exchangeCodeForToken(code);
|
||||
}
|
||||
|
||||
setAccessToken(accessToken: string, refreshToken?: string): void {
|
||||
this.auth.setAccessToken(accessToken, refreshToken);
|
||||
}
|
||||
|
||||
isAuthenticated(): boolean {
|
||||
return this.auth.isAuthenticated();
|
||||
}
|
||||
}
|
||||
226
src/setup/interactive-setup.ts
Normal file
226
src/setup/interactive-setup.ts
Normal file
|
|
@ -0,0 +1,226 @@
|
|||
import * as readline from 'readline';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
import { TickTickAuth } from '../auth/ticktick-auth.js';
|
||||
|
||||
export interface SetupConfig {
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
redirectUri: string;
|
||||
accessToken?: string;
|
||||
refreshToken?: string;
|
||||
}
|
||||
|
||||
export class InteractiveSetup {
|
||||
private rl: readline.Interface;
|
||||
private configDir: string;
|
||||
private configPath: string;
|
||||
|
||||
constructor() {
|
||||
this.rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout
|
||||
});
|
||||
|
||||
this.configDir = path.join(os.homedir(), '.ticktick-mcp');
|
||||
this.configPath = path.join(this.configDir, 'config.json');
|
||||
}
|
||||
|
||||
private question(prompt: string): Promise<string> {
|
||||
return new Promise((resolve) => {
|
||||
this.rl.question(prompt, (answer) => {
|
||||
resolve(answer.trim());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private async displayWelcome() {
|
||||
console.log('\n🎉 TickTick MCP Server セットアップ');
|
||||
console.log('=====================================\n');
|
||||
console.log('このセットアップでは、TickTick APIの認証情報を設定します。');
|
||||
console.log('完了後、Claude DesktopやMCP Inspectorで実際のTickTickデータを使用できます。\n');
|
||||
}
|
||||
|
||||
private async displayApiCredentialsInstructions() {
|
||||
console.log('📋 TickTick API認証情報の取得方法:');
|
||||
console.log('-----------------------------------');
|
||||
console.log('1. https://developer.ticktick.com/ にアクセス');
|
||||
console.log('2. TickTickアカウントでログイン');
|
||||
console.log('3. 右上の「Manage Apps」をクリック');
|
||||
console.log('4. 「+App Name」をクリックして新しいアプリを作成');
|
||||
console.log('5. アプリ名を入力(例:「My MCP Server」)');
|
||||
console.log('6. Client IDとClient Secretをコピー');
|
||||
console.log('7. OAuth Redirect URLを http://localhost:3000/callback に設定\n');
|
||||
|
||||
const proceed = await this.question('上記の手順を完了しましたか? (y/N): ');
|
||||
if (proceed.toLowerCase() !== 'y' && proceed.toLowerCase() !== 'yes') {
|
||||
console.log('\n手順を完了してから再度実行してください。');
|
||||
process.exit(0);
|
||||
}
|
||||
console.log('');
|
||||
}
|
||||
|
||||
private async collectCredentials(): Promise<SetupConfig> {
|
||||
console.log('🔑 API認証情報の入力');
|
||||
console.log('---------------------');
|
||||
|
||||
const clientId = await this.question('Client ID: ');
|
||||
if (!clientId) {
|
||||
throw new Error('Client IDは必須です');
|
||||
}
|
||||
|
||||
const clientSecret = await this.question('Client Secret: ');
|
||||
if (!clientSecret) {
|
||||
throw new Error('Client Secretは必須です');
|
||||
}
|
||||
|
||||
const redirectUri = await this.question('Redirect URI (default: http://localhost:3000/callback): ') || 'http://localhost:3000/callback';
|
||||
|
||||
return {
|
||||
clientId,
|
||||
clientSecret,
|
||||
redirectUri
|
||||
};
|
||||
}
|
||||
|
||||
private async performOAuthFlow(config: SetupConfig): Promise<SetupConfig> {
|
||||
console.log('\n🔐 OAuth認証フロー');
|
||||
console.log('-------------------');
|
||||
|
||||
const auth = new TickTickAuth({
|
||||
clientId: config.clientId,
|
||||
clientSecret: config.clientSecret,
|
||||
redirectUri: config.redirectUri
|
||||
});
|
||||
|
||||
const authUrl = auth.getAuthorizationUrl();
|
||||
console.log('\n1. 以下のURLをブラウザで開いてください:');
|
||||
console.log(` ${authUrl}\n`);
|
||||
console.log('2. TickTickでアプリを承認');
|
||||
console.log('3. リダイレクト後のURLからauthorization codeをコピー');
|
||||
console.log(' (例: http://localhost:3000/callback?code=XXXXXX の XXXXXXの部分)\n');
|
||||
|
||||
const authCode = await this.question('Authorization code: ');
|
||||
if (!authCode) {
|
||||
throw new Error('Authorization codeは必須です');
|
||||
}
|
||||
|
||||
console.log('\n🔄 アクセストークンを取得中...');
|
||||
try {
|
||||
const tokens = await auth.exchangeCodeForToken(authCode);
|
||||
|
||||
console.log('✅ 認証成功!');
|
||||
|
||||
return {
|
||||
...config,
|
||||
accessToken: tokens.access_token,
|
||||
refreshToken: tokens.refresh_token
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('❌ 認証エラー:', error);
|
||||
throw new Error('OAuth認証に失敗しました。Client IDとClient Secretを確認してください。');
|
||||
}
|
||||
}
|
||||
|
||||
private async saveConfig(config: SetupConfig) {
|
||||
console.log('\n💾 設定を保存中...');
|
||||
|
||||
// Create config directory if it doesn't exist
|
||||
if (!fs.existsSync(this.configDir)) {
|
||||
fs.mkdirSync(this.configDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Save config to file
|
||||
fs.writeFileSync(this.configPath, JSON.stringify(config, null, 2));
|
||||
|
||||
// Set file permissions (readable only by owner)
|
||||
fs.chmodSync(this.configPath, 0o600);
|
||||
|
||||
console.log(`✅ 設定ファイルを保存しました: ${this.configPath}`);
|
||||
}
|
||||
|
||||
private async displayCompletion() {
|
||||
console.log('\n🎉 セットアップ完了!');
|
||||
console.log('===================\n');
|
||||
console.log('次の手順:');
|
||||
console.log('1. TickTick MCP Serverを起動:');
|
||||
console.log(' npx @ticktick-ecosystem/mcp-server\n');
|
||||
console.log('2. Claude Desktopで使用する場合:');
|
||||
console.log(' 設定ファイルに以下を追加:');
|
||||
console.log(' {');
|
||||
console.log(' "mcpServers": {');
|
||||
console.log(' "ticktick": {');
|
||||
console.log(' "command": "npx",');
|
||||
console.log(' "args": ["@ticktick-ecosystem/mcp-server"]');
|
||||
console.log(' }');
|
||||
console.log(' }');
|
||||
console.log(' }\n');
|
||||
console.log('3. MCP Inspectorでテスト:');
|
||||
console.log(' npx @modelcontextprotocol/inspector npx @ticktick-ecosystem/mcp-server\n');
|
||||
console.log('これで実際のTickTickデータでAI支援のタスク管理が可能になります!');
|
||||
}
|
||||
|
||||
public async run(): Promise<void> {
|
||||
try {
|
||||
await this.displayWelcome();
|
||||
|
||||
// Check if config already exists
|
||||
if (fs.existsSync(this.configPath)) {
|
||||
console.log('⚠️ 既存の設定が見つかりました。');
|
||||
const overwrite = await this.question('設定を上書きしますか? (y/N): ');
|
||||
if (overwrite.toLowerCase() !== 'y' && overwrite.toLowerCase() !== 'yes') {
|
||||
console.log('セットアップをキャンセルしました。');
|
||||
this.rl.close();
|
||||
return;
|
||||
}
|
||||
console.log('');
|
||||
}
|
||||
|
||||
await this.displayApiCredentialsInstructions();
|
||||
|
||||
const credentials = await this.collectCredentials();
|
||||
|
||||
const completeConfig = await this.performOAuthFlow(credentials);
|
||||
|
||||
await this.saveConfig(completeConfig);
|
||||
|
||||
await this.displayCompletion();
|
||||
|
||||
} catch (error) {
|
||||
console.error('\n❌ セットアップエラー:', error instanceof Error ? error.message : error);
|
||||
console.log('\nエラーが発生しました。手順を確認して再度実行してください。');
|
||||
process.exit(1);
|
||||
} finally {
|
||||
this.rl.close();
|
||||
}
|
||||
}
|
||||
|
||||
public static loadConfig(): SetupConfig | null {
|
||||
const configDir = path.join(os.homedir(), '.ticktick-mcp');
|
||||
const configPath = path.join(configDir, 'config.json');
|
||||
|
||||
if (!fs.existsSync(configPath)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const configData = fs.readFileSync(configPath, 'utf8');
|
||||
return JSON.parse(configData);
|
||||
} catch (error) {
|
||||
console.error('設定ファイルの読み込みエラー:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static hasConfig(): boolean {
|
||||
const configDir = path.join(os.homedir(), '.ticktick-mcp');
|
||||
const configPath = path.join(configDir, 'config.json');
|
||||
return fs.existsSync(configPath);
|
||||
}
|
||||
|
||||
public static getConfigPath(): string {
|
||||
const configDir = path.join(os.homedir(), '.ticktick-mcp');
|
||||
return path.join(configDir, 'config.json');
|
||||
}
|
||||
}
|
||||
215
src/tools/project-tools.ts
Normal file
215
src/tools/project-tools.ts
Normal file
|
|
@ -0,0 +1,215 @@
|
|||
import { Tool } from '@modelcontextprotocol/sdk/types.js';
|
||||
import { z } from 'zod';
|
||||
import { ITickTickAPI } from '../types/api-interface.js';
|
||||
|
||||
// Zod schemas for tool arguments validation
|
||||
const CreateProjectSchema = z.object({
|
||||
name: z.string().describe('Name of the project'),
|
||||
color: z.string().optional().describe('Color of the project (hex code or color name)'),
|
||||
groupId: z.string().optional().describe('ID of the group to add project to'),
|
||||
});
|
||||
|
||||
const UpdateProjectSchema = z.object({
|
||||
projectId: z.string().describe('ID of the project to update'),
|
||||
name: z.string().optional().describe('New name for the project'),
|
||||
color: z.string().optional().describe('New color for the project'),
|
||||
});
|
||||
|
||||
const ProjectIdSchema = z.object({
|
||||
projectId: z.string().describe('ID of the project'),
|
||||
});
|
||||
|
||||
export class ProjectTools {
|
||||
constructor(private api: ITickTickAPI) {}
|
||||
|
||||
getTools(): Tool[] {
|
||||
return [
|
||||
{
|
||||
name: 'get_projects',
|
||||
description: 'Get all projects',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {},
|
||||
required: [],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'get_project',
|
||||
description: 'Get a specific project by ID',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
projectId: {
|
||||
type: 'string',
|
||||
description: 'ID of the project to retrieve',
|
||||
},
|
||||
},
|
||||
required: ['projectId'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'create_project',
|
||||
description: 'Create a new project',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: {
|
||||
type: 'string',
|
||||
description: 'Name of the project',
|
||||
},
|
||||
color: {
|
||||
type: 'string',
|
||||
description: 'Color of the project (hex code or color name)',
|
||||
},
|
||||
groupId: {
|
||||
type: 'string',
|
||||
description: 'ID of the group to add project to',
|
||||
},
|
||||
},
|
||||
required: ['name'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'update_project',
|
||||
description: 'Update an existing project',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
projectId: {
|
||||
type: 'string',
|
||||
description: 'ID of the project to update',
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
description: 'New name for the project',
|
||||
},
|
||||
color: {
|
||||
type: 'string',
|
||||
description: 'New color for the project',
|
||||
},
|
||||
},
|
||||
required: ['projectId'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'delete_project',
|
||||
description: 'Delete a project',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
projectId: {
|
||||
type: 'string',
|
||||
description: 'ID of the project to delete',
|
||||
},
|
||||
},
|
||||
required: ['projectId'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'get_project_tasks',
|
||||
description: 'Get all tasks in a specific project',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
projectId: {
|
||||
type: 'string',
|
||||
description: 'ID of the project',
|
||||
},
|
||||
completed: {
|
||||
type: 'boolean',
|
||||
description: 'Filter by completion status',
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
description: 'Maximum number of tasks to return',
|
||||
},
|
||||
},
|
||||
required: ['projectId'],
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
async handleToolCall(name: string, args: any): Promise<any> {
|
||||
try {
|
||||
switch (name) {
|
||||
case 'get_projects': {
|
||||
const projects = await this.api.getProjects();
|
||||
return {
|
||||
success: true,
|
||||
projects,
|
||||
count: projects.length,
|
||||
};
|
||||
}
|
||||
|
||||
case 'get_project': {
|
||||
const validated = ProjectIdSchema.parse(args);
|
||||
const project = await this.api.getProject(validated.projectId);
|
||||
return {
|
||||
success: true,
|
||||
project,
|
||||
};
|
||||
}
|
||||
|
||||
case 'create_project': {
|
||||
const validated = CreateProjectSchema.parse(args);
|
||||
const project = await this.api.createProject(validated);
|
||||
return {
|
||||
success: true,
|
||||
project,
|
||||
message: `Project "${project.name}" created successfully`,
|
||||
};
|
||||
}
|
||||
|
||||
case 'update_project': {
|
||||
const validated = UpdateProjectSchema.parse(args);
|
||||
const { projectId, ...updateData } = validated;
|
||||
const project = await this.api.updateProject(projectId, updateData);
|
||||
return {
|
||||
success: true,
|
||||
project,
|
||||
message: `Project "${project.name}" updated successfully`,
|
||||
};
|
||||
}
|
||||
|
||||
case 'delete_project': {
|
||||
const validated = ProjectIdSchema.parse(args);
|
||||
await this.api.deleteProject(validated.projectId);
|
||||
return {
|
||||
success: true,
|
||||
message: `Project deleted successfully`,
|
||||
};
|
||||
}
|
||||
|
||||
case 'get_project_tasks': {
|
||||
const validated = z.object({
|
||||
projectId: z.string(),
|
||||
completed: z.boolean().optional(),
|
||||
limit: z.number().optional(),
|
||||
}).parse(args);
|
||||
|
||||
const tasks = await this.api.getTasks({
|
||||
projectId: validated.projectId,
|
||||
completed: validated.completed,
|
||||
limit: validated.limit,
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
tasks,
|
||||
count: tasks.length,
|
||||
projectId: validated.projectId,
|
||||
};
|
||||
}
|
||||
|
||||
default:
|
||||
throw new Error(`Unknown tool: ${name}`);
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
331
src/tools/task-tools.ts
Normal file
331
src/tools/task-tools.ts
Normal file
|
|
@ -0,0 +1,331 @@
|
|||
import { Tool } from '@modelcontextprotocol/sdk/types.js';
|
||||
import { z } from 'zod';
|
||||
import { ITickTickAPI } from '../types/api-interface.js';
|
||||
|
||||
// Zod schemas for tool arguments validation
|
||||
const CreateTaskSchema = z.object({
|
||||
title: z.string().describe('Title of the task'),
|
||||
content: z.string().optional().describe('Description or content of the task'),
|
||||
dueDate: z.string().optional().describe('Due date in ISO format (YYYY-MM-DD or YYYY-MM-DDTHH:mm:ss)'),
|
||||
priority: z.number().min(0).max(5).optional().describe('Priority level (0=None, 1=Low, 3=Medium, 5=High)'),
|
||||
projectId: z.string().optional().describe('ID of the project to add task to'),
|
||||
tags: z.array(z.string()).optional().describe('Array of tags for the task'),
|
||||
allDay: z.boolean().optional().describe('Whether this is an all-day task'),
|
||||
});
|
||||
|
||||
const UpdateTaskSchema = z.object({
|
||||
taskId: z.string().describe('ID of the task to update'),
|
||||
title: z.string().optional().describe('New title for the task'),
|
||||
content: z.string().optional().describe('New description or content'),
|
||||
dueDate: z.string().optional().describe('New due date in ISO format'),
|
||||
priority: z.number().min(0).max(5).optional().describe('New priority level'),
|
||||
projectId: z.string().optional().describe('New project ID'),
|
||||
tags: z.array(z.string()).optional().describe('New tags array'),
|
||||
allDay: z.boolean().optional().describe('Whether this is an all-day task'),
|
||||
});
|
||||
|
||||
const GetTasksSchema = z.object({
|
||||
projectId: z.string().optional().describe('Filter tasks by project ID'),
|
||||
completed: z.boolean().optional().describe('Filter by completion status'),
|
||||
limit: z.number().optional().describe('Maximum number of tasks to return'),
|
||||
startDate: z.string().optional().describe('Start date filter (YYYY-MM-DD)'),
|
||||
endDate: z.string().optional().describe('End date filter (YYYY-MM-DD)'),
|
||||
});
|
||||
|
||||
const TaskIdSchema = z.object({
|
||||
taskId: z.string().describe('ID of the task'),
|
||||
});
|
||||
|
||||
const SearchTasksSchema = z.object({
|
||||
query: z.string().describe('Search query for task title or content'),
|
||||
limit: z.number().optional().describe('Maximum number of results to return'),
|
||||
});
|
||||
|
||||
export class TaskTools {
|
||||
constructor(private api: ITickTickAPI) {}
|
||||
|
||||
getTools(): Tool[] {
|
||||
return [
|
||||
{
|
||||
name: 'create_task',
|
||||
description: 'Create a new task in TickTick',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
title: {
|
||||
type: 'string',
|
||||
description: 'Title of the task',
|
||||
},
|
||||
content: {
|
||||
type: 'string',
|
||||
description: 'Description or content of the task',
|
||||
},
|
||||
dueDate: {
|
||||
type: 'string',
|
||||
description: 'Due date in ISO format (YYYY-MM-DD or YYYY-MM-DDTHH:mm:ss)',
|
||||
},
|
||||
priority: {
|
||||
type: 'number',
|
||||
description: 'Priority level (0=None, 1=Low, 3=Medium, 5=High)',
|
||||
minimum: 0,
|
||||
maximum: 5,
|
||||
},
|
||||
projectId: {
|
||||
type: 'string',
|
||||
description: 'ID of the project to add task to',
|
||||
},
|
||||
tags: {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
description: 'Array of tags for the task',
|
||||
},
|
||||
allDay: {
|
||||
type: 'boolean',
|
||||
description: 'Whether this is an all-day task',
|
||||
},
|
||||
},
|
||||
required: ['title'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'get_tasks',
|
||||
description: 'Get tasks with optional filtering',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
projectId: {
|
||||
type: 'string',
|
||||
description: 'Filter tasks by project ID',
|
||||
},
|
||||
completed: {
|
||||
type: 'boolean',
|
||||
description: 'Filter by completion status',
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
description: 'Maximum number of tasks to return',
|
||||
},
|
||||
startDate: {
|
||||
type: 'string',
|
||||
description: 'Start date filter (YYYY-MM-DD)',
|
||||
},
|
||||
endDate: {
|
||||
type: 'string',
|
||||
description: 'End date filter (YYYY-MM-DD)',
|
||||
},
|
||||
},
|
||||
required: [],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'update_task',
|
||||
description: 'Update an existing task',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
taskId: {
|
||||
type: 'string',
|
||||
description: 'ID of the task to update',
|
||||
},
|
||||
title: {
|
||||
type: 'string',
|
||||
description: 'New title for the task',
|
||||
},
|
||||
content: {
|
||||
type: 'string',
|
||||
description: 'New description or content',
|
||||
},
|
||||
dueDate: {
|
||||
type: 'string',
|
||||
description: 'New due date in ISO format',
|
||||
},
|
||||
priority: {
|
||||
type: 'number',
|
||||
description: 'New priority level',
|
||||
minimum: 0,
|
||||
maximum: 5,
|
||||
},
|
||||
projectId: {
|
||||
type: 'string',
|
||||
description: 'New project ID',
|
||||
},
|
||||
tags: {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
description: 'New tags array',
|
||||
},
|
||||
allDay: {
|
||||
type: 'boolean',
|
||||
description: 'Whether this is an all-day task',
|
||||
},
|
||||
},
|
||||
required: ['taskId'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'complete_task',
|
||||
description: 'Mark a task as completed',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
taskId: {
|
||||
type: 'string',
|
||||
description: 'ID of the task to complete',
|
||||
},
|
||||
},
|
||||
required: ['taskId'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'delete_task',
|
||||
description: 'Delete a task',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
taskId: {
|
||||
type: 'string',
|
||||
description: 'ID of the task to delete',
|
||||
},
|
||||
},
|
||||
required: ['taskId'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'get_today_tasks',
|
||||
description: 'Get all tasks scheduled for today',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {},
|
||||
required: [],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'get_overdue_tasks',
|
||||
description: 'Get all overdue tasks',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {},
|
||||
required: [],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'search_tasks',
|
||||
description: 'Search tasks by title or content',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
query: {
|
||||
type: 'string',
|
||||
description: 'Search query for task title or content',
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
description: 'Maximum number of results to return',
|
||||
},
|
||||
},
|
||||
required: ['query'],
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
async handleToolCall(name: string, args: any): Promise<any> {
|
||||
try {
|
||||
switch (name) {
|
||||
case 'create_task': {
|
||||
const validated = CreateTaskSchema.parse(args);
|
||||
const task = await this.api.createTask(validated);
|
||||
return {
|
||||
success: true,
|
||||
task,
|
||||
message: `Task "${task.title}" created successfully`,
|
||||
};
|
||||
}
|
||||
|
||||
case 'get_tasks': {
|
||||
const validated = GetTasksSchema.parse(args);
|
||||
const tasks = await this.api.getTasks(validated);
|
||||
return {
|
||||
success: true,
|
||||
tasks,
|
||||
count: tasks.length,
|
||||
};
|
||||
}
|
||||
|
||||
case 'update_task': {
|
||||
const validated = UpdateTaskSchema.parse(args);
|
||||
const { taskId, ...updateData } = validated;
|
||||
const task = await this.api.updateTask({ id: taskId, ...updateData });
|
||||
return {
|
||||
success: true,
|
||||
task,
|
||||
message: `Task "${task.title}" updated successfully`,
|
||||
};
|
||||
}
|
||||
|
||||
case 'complete_task': {
|
||||
const validated = TaskIdSchema.parse(args);
|
||||
const task = await this.api.completeTask(validated.taskId);
|
||||
return {
|
||||
success: true,
|
||||
task,
|
||||
message: `Task completed successfully`,
|
||||
};
|
||||
}
|
||||
|
||||
case 'delete_task': {
|
||||
const validated = TaskIdSchema.parse(args);
|
||||
await this.api.deleteTask(validated.taskId);
|
||||
return {
|
||||
success: true,
|
||||
message: `Task deleted successfully`,
|
||||
};
|
||||
}
|
||||
|
||||
case 'get_today_tasks': {
|
||||
const tasks = await this.api.getTodayTasks();
|
||||
return {
|
||||
success: true,
|
||||
tasks,
|
||||
count: tasks.length,
|
||||
date: new Date().toISOString().split('T')[0],
|
||||
};
|
||||
}
|
||||
|
||||
case 'get_overdue_tasks': {
|
||||
const tasks = await this.api.getOverdueTasks();
|
||||
return {
|
||||
success: true,
|
||||
tasks,
|
||||
count: tasks.length,
|
||||
};
|
||||
}
|
||||
|
||||
case 'search_tasks': {
|
||||
const validated = SearchTasksSchema.parse(args);
|
||||
const tasks = await this.api.searchTasks(validated.query);
|
||||
|
||||
// Apply limit if specified
|
||||
const limitedTasks = validated.limit
|
||||
? tasks.slice(0, validated.limit)
|
||||
: tasks;
|
||||
|
||||
return {
|
||||
success: true,
|
||||
tasks: limitedTasks,
|
||||
count: limitedTasks.length,
|
||||
query: validated.query,
|
||||
};
|
||||
}
|
||||
|
||||
default:
|
||||
throw new Error(`Unknown tool: ${name}`);
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
21
src/types/api-interface.ts
Normal file
21
src/types/api-interface.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import { Task, Project } from './ticktick.js';
|
||||
|
||||
export interface ITickTickAPI {
|
||||
// Task operations
|
||||
getTasks(filter?: any): Promise<Task[]>;
|
||||
getTask(taskId: string): Promise<Task>;
|
||||
createTask(task: any): Promise<Task>;
|
||||
updateTask(task: any): Promise<Task>;
|
||||
deleteTask(taskId: string): Promise<void>;
|
||||
completeTask(taskId: string): Promise<Task>;
|
||||
searchTasks(query: string, limit?: number): Promise<Task[]>;
|
||||
getTodayTasks(): Promise<Task[]>;
|
||||
getOverdueTasks(): Promise<Task[]>;
|
||||
|
||||
// Project operations
|
||||
getProjects(): Promise<Project[]>;
|
||||
getProject(projectId: string): Promise<Project>;
|
||||
createProject(project: any): Promise<Project>;
|
||||
updateProject(projectId: string, projectData: any): Promise<Project>;
|
||||
deleteProject(projectId: string): Promise<void>;
|
||||
}
|
||||
130
src/types/ticktick.ts
Normal file
130
src/types/ticktick.ts
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
// TickTick API Types
|
||||
export interface TickTickConfig {
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
redirectUri: string;
|
||||
accessToken?: string;
|
||||
refreshToken?: string;
|
||||
}
|
||||
|
||||
export interface Task {
|
||||
id: string;
|
||||
title: string;
|
||||
content?: string;
|
||||
desc?: string;
|
||||
allDay?: boolean;
|
||||
startDate?: string;
|
||||
dueDate?: string;
|
||||
timeZone?: string;
|
||||
reminders?: string[];
|
||||
repeat?: string;
|
||||
priority?: number;
|
||||
sortOrder?: number;
|
||||
items?: TaskItem[];
|
||||
progress?: number;
|
||||
assignee?: string;
|
||||
projectId: string;
|
||||
tags?: string[];
|
||||
kind?: string;
|
||||
createdTime?: string;
|
||||
modifiedTime?: string;
|
||||
completedTime?: string;
|
||||
status?: number;
|
||||
}
|
||||
|
||||
export interface TaskItem {
|
||||
id: string;
|
||||
title: string;
|
||||
status: number;
|
||||
completedTime?: string;
|
||||
sortOrder: number;
|
||||
}
|
||||
|
||||
export interface Project {
|
||||
id: string;
|
||||
name: string;
|
||||
color?: string;
|
||||
inAll?: boolean;
|
||||
sortOrder?: number;
|
||||
sortType?: string;
|
||||
userCount?: number;
|
||||
etag?: string;
|
||||
modifiedTime?: string;
|
||||
closed?: boolean;
|
||||
muted?: boolean;
|
||||
transferred?: string;
|
||||
groupId?: string;
|
||||
viewMode?: string;
|
||||
notificationOptions?: {
|
||||
[key: string]: boolean;
|
||||
};
|
||||
teamId?: string;
|
||||
permission?: string;
|
||||
kind?: string;
|
||||
}
|
||||
|
||||
export interface CreateTaskRequest {
|
||||
title: string;
|
||||
content?: string;
|
||||
desc?: string;
|
||||
allDay?: boolean;
|
||||
startDate?: string;
|
||||
dueDate?: string;
|
||||
timeZone?: string;
|
||||
priority?: number;
|
||||
projectId?: string;
|
||||
tags?: string[];
|
||||
reminders?: string[];
|
||||
}
|
||||
|
||||
export interface UpdateTaskRequest {
|
||||
id: string;
|
||||
title?: string;
|
||||
content?: string;
|
||||
desc?: string;
|
||||
allDay?: boolean;
|
||||
startDate?: string;
|
||||
dueDate?: string;
|
||||
timeZone?: string;
|
||||
priority?: number;
|
||||
projectId?: string;
|
||||
tags?: string[];
|
||||
reminders?: string[];
|
||||
status?: number;
|
||||
}
|
||||
|
||||
export interface CreateProjectRequest {
|
||||
name: string;
|
||||
color?: string;
|
||||
groupId?: string;
|
||||
}
|
||||
|
||||
export interface UpdateProjectRequest {
|
||||
id: string;
|
||||
name?: string;
|
||||
color?: string;
|
||||
groupId?: string;
|
||||
}
|
||||
|
||||
export interface TaskFilter {
|
||||
projectId?: string;
|
||||
completed?: boolean;
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
}
|
||||
|
||||
export interface AuthTokens {
|
||||
access_token: string;
|
||||
refresh_token: string;
|
||||
token_type: string;
|
||||
expires_in: number;
|
||||
scope: string;
|
||||
}
|
||||
|
||||
export interface APIResponse<T> {
|
||||
data?: T;
|
||||
error?: string;
|
||||
status: number;
|
||||
}
|
||||
83
test-build.js
Normal file
83
test-build.js
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const { spawn } = require('child_process');
|
||||
const path = require('path');
|
||||
|
||||
function runCommand(command, args = [], options = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
console.log(`Running: ${command} ${args.join(' ')}`);
|
||||
|
||||
const child = spawn(command, args, {
|
||||
stdio: 'inherit',
|
||||
shell: true,
|
||||
...options
|
||||
});
|
||||
|
||||
child.on('close', (code) => {
|
||||
if (code === 0) {
|
||||
resolve(code);
|
||||
} else {
|
||||
reject(new Error(`Command failed with exit code ${code}`));
|
||||
}
|
||||
});
|
||||
|
||||
child.on('error', (error) => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const projectDir = process.cwd();
|
||||
console.log(`Building TickTick MCP Server in: ${projectDir}`);
|
||||
console.log('=====================================');
|
||||
|
||||
try {
|
||||
// Check if node_modules exists
|
||||
console.log('\n1. Checking dependencies...');
|
||||
const fs = require('fs');
|
||||
if (!fs.existsSync('node_modules')) {
|
||||
console.log('node_modules not found. Installing dependencies...');
|
||||
await runCommand('npm', ['install']);
|
||||
} else {
|
||||
console.log('✅ node_modules exists');
|
||||
}
|
||||
|
||||
// Clean dist directory
|
||||
console.log('\n2. Cleaning build directory...');
|
||||
if (fs.existsSync('dist')) {
|
||||
await runCommand('rm', ['-rf', 'dist']);
|
||||
}
|
||||
console.log('✅ Build directory cleaned');
|
||||
|
||||
// Run TypeScript compilation
|
||||
console.log('\n3. Running TypeScript compilation...');
|
||||
await runCommand('npx', ['tsc', '--noEmit'], { cwd: projectDir });
|
||||
console.log('✅ TypeScript type checking passed');
|
||||
|
||||
console.log('\n4. Building project...');
|
||||
await runCommand('npx', ['tsc'], { cwd: projectDir });
|
||||
console.log('✅ Build completed successfully');
|
||||
|
||||
// Check if dist was created
|
||||
if (fs.existsSync('dist')) {
|
||||
console.log('\n5. Verifying build output...');
|
||||
const distFiles = fs.readdirSync('dist');
|
||||
console.log('Built files:', distFiles);
|
||||
console.log('✅ Build verification complete');
|
||||
}
|
||||
|
||||
console.log('\n🎉 Build successful!');
|
||||
console.log('=====================================');
|
||||
|
||||
} catch (error) {
|
||||
console.error('\n❌ Build failed!');
|
||||
console.error('Error:', error.message);
|
||||
console.error('=====================================');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
main();
|
||||
}
|
||||
17
test-compile.ts
Normal file
17
test-compile.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
// Simple test file to check TypeScript compilation
|
||||
import { TickTickMCPServer } from './src/server.js';
|
||||
import { TickTickConfig } from './src/types/ticktick.js';
|
||||
|
||||
// Test basic imports
|
||||
console.log('TypeScript imports working');
|
||||
|
||||
// Test config type
|
||||
const testConfig: TickTickConfig = {
|
||||
clientId: 'test',
|
||||
clientSecret: 'test',
|
||||
redirectUri: 'test'
|
||||
};
|
||||
|
||||
console.log('Config type working');
|
||||
|
||||
export {};
|
||||
49
test-demo.js
Normal file
49
test-demo.js
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
// Simple test script to verify demo mode functionality
|
||||
import { spawn } from 'child_process';
|
||||
import { setTimeout } from 'timers/promises';
|
||||
|
||||
console.log('🧪 Testing TickTick MCP Server Demo Mode...\n');
|
||||
|
||||
// Start the server in demo mode
|
||||
const server = spawn('node', ['dist/index.js', '--demo'], {
|
||||
stdio: ['pipe', 'pipe', 'inherit'],
|
||||
env: { ...process.env, TICKTICK_DEMO_MODE: 'true' }
|
||||
});
|
||||
|
||||
// Send a simple MCP request to list tools
|
||||
const mcpRequest = {
|
||||
jsonrpc: '2.0',
|
||||
id: 1,
|
||||
method: 'tools/list'
|
||||
};
|
||||
|
||||
setTimeout(1000).then(() => {
|
||||
console.log('📤 Sending tools/list request...');
|
||||
server.stdin.write(JSON.stringify(mcpRequest) + '\n');
|
||||
|
||||
setTimeout(2000).then(() => {
|
||||
console.log('✅ Demo mode test completed');
|
||||
server.kill();
|
||||
process.exit(0);
|
||||
});
|
||||
});
|
||||
|
||||
server.stdout.on('data', (data) => {
|
||||
try {
|
||||
const response = JSON.parse(data.toString());
|
||||
console.log('📥 Server response:', JSON.stringify(response, null, 2));
|
||||
} catch (e) {
|
||||
console.log('📥 Server output:', data.toString());
|
||||
}
|
||||
});
|
||||
|
||||
server.on('error', (error) => {
|
||||
console.error('❌ Server error:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
server.on('exit', (code) => {
|
||||
console.log(`\n🏁 Server exited with code: ${code}`);
|
||||
});
|
||||
26
tsconfig.json
Normal file
26
tsconfig.json
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "node",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"declaration": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"removeComments": true,
|
||||
"sourceMap": true
|
||||
},
|
||||
"include": [
|
||||
"src/**/*"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist",
|
||||
"**/*.test.ts"
|
||||
]
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue