Initial commit: TickTick MCP Server with interactive setup

This commit is contained in:
柴田貴司 2025-09-22 18:50:15 +09:00
commit 748bf79ab4
40 changed files with 10855 additions and 0 deletions

22
.env.example Normal file
View 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
View 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
View 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
View 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
View 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.
[![npm version](https://badge.fury.io/js/%40ticktick-ecosystem%2Fmcp-server.svg)](https://badge.fury.io/js/%40ticktick-ecosystem%2Fmcp-server)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![Node.js Version](https://img.shields.io/badge/node-%3E%3D18-brightgreen)](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
View 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)

View 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
View 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
View 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
View 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();
}

View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load diff

94
package.json Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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',
};
}
}

View 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
View 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
View 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);
});

View 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)}`;
}
}
}

View 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
View 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();
}
}

View 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
View 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
View 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),
};
}
}
}

View 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
View 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
View 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
View 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
View 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
View 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"
]
}