This is page 1 of 2. Use http://codebase.md/deus-h/claudeus-plane-mcp?lines=true&page={x} to view the full context. # Directory Structure ``` ├── .cursorignore ├── .env.example ├── .eslintrc.json ├── .gitignore ├── .prettierrc ├── CONTRIBUTING.md ├── Dockerfile ├── docs │ ├── smithery-docs.md │ └── transform-to-proper-standards.md ├── eslint.config.js ├── jest.config.js ├── LICENSE ├── notes.txt ├── package.json ├── plane-instances-example.json ├── plane-instances-test-example.json ├── pnpm-lock.yaml ├── readme.md ├── SECURITY.md ├── smithery.yaml ├── src │ ├── api │ │ ├── base-client.ts │ │ ├── client.ts │ │ ├── issues │ │ │ ├── client.ts │ │ │ └── types.ts │ │ ├── projects.ts │ │ └── types │ │ ├── config.ts │ │ └── project.ts │ ├── config │ │ └── plane-config.ts │ ├── dummy-data │ │ ├── json.d.ts │ │ ├── projects.d.ts │ │ └── projects.json │ ├── index.ts │ ├── inspector-wrapper.ts │ ├── mcp │ │ ├── server.ts │ │ └── tools.ts │ ├── prompts │ │ └── projects │ │ ├── definitions.ts │ │ ├── handlers.ts │ │ └── index.ts │ ├── security │ │ └── SecurityManager.ts │ ├── test │ │ ├── integration │ │ │ └── projects.test.ts │ │ ├── mcp-test-harness.ts │ │ ├── setup.ts │ │ └── unit │ │ └── tools │ │ └── projects │ │ └── list.test.ts │ ├── tools │ │ ├── index.ts │ │ ├── issues │ │ │ ├── create.ts │ │ │ ├── get.ts │ │ │ ├── list.ts │ │ │ └── update.ts │ │ └── projects │ │ ├── __tests__ │ │ │ ├── create.test.ts │ │ │ ├── delete.test.ts │ │ │ ├── handlers.test.ts │ │ │ └── update.test.ts │ │ ├── create.ts │ │ ├── delete.ts │ │ ├── handlers.ts │ │ ├── index.ts │ │ ├── list.ts │ │ └── update.ts │ └── types │ ├── api.ts │ ├── index.ts │ ├── issue.ts │ ├── mcp.d.ts │ ├── mcp.ts │ ├── project.ts │ ├── prompt.ts │ └── security.ts ├── tsconfig.json └── vitest.config.ts ``` # Files -------------------------------------------------------------------------------- /.cursorignore: -------------------------------------------------------------------------------- ``` 1 | # Add directories or file patterns to ignore during indexing (e.g. foo/ or *.csv) 2 | ``` -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- ``` 1 | { 2 | "semi": true, 3 | "trailingComma": "es5", 4 | "singleQuote": true, 5 | "printWidth": 100, 6 | "tabWidth": 2, 7 | "useTabs": false 8 | } ``` -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- ``` 1 | # Server Configuration 2 | PORT=3000 3 | HOST=localhost 4 | 5 | # Logging 6 | LOG_LEVEL=info 7 | 8 | # Plane API Configuration 9 | PLANE_INSTANCES_PATH=./plane-instances.json 10 | 11 | # Security 12 | MAX_REQUEST_SIZE=10mb 13 | RATE_LIMIT_WINDOW=15m 14 | RATE_LIMIT_MAX=100 ``` -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` 1 | # Dependencies 2 | node_modules/ 3 | .pnpm-store/ 4 | 5 | # Build 6 | dist/ 7 | build/ 8 | 9 | # Environment 10 | .env 11 | plane-instances.json 12 | plane-instances-test.json 13 | 14 | # IDE 15 | .idea/ 16 | .vscode/ 17 | *.swp 18 | *.swo 19 | 20 | # Logs 21 | logs/ 22 | *.log 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | pnpm-debug.log* 27 | 28 | # Testing 29 | coverage/ 30 | .nyc_output/ 31 | 32 | # OS 33 | .DS_Store 34 | Thumbs.db 35 | 36 | # Temporary files 37 | *.tmp 38 | *.temp 39 | .cache/ 40 | ``` -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "extends": [ 4 | "eslint:recommended", 5 | "plugin:@typescript-eslint/recommended" 6 | ], 7 | "parserOptions": { 8 | "ecmaVersion": 2022, 9 | "sourceType": "module" 10 | }, 11 | "rules": { 12 | "@typescript-eslint/explicit-function-return-type": "warn", 13 | "@typescript-eslint/no-explicit-any": "warn", 14 | "@typescript-eslint/no-unused-vars": ["warn", { "argsIgnorePattern": "^_" }] 15 | } 16 | } ``` -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- ```markdown 1 | ⚠️ **PRIVATE REPOSITORY NOTICE** ⚠️ 2 | 3 | This is a private repository for SimHop IT & Media AB team members only. While the code is available for viewing and use under the MIT license, we do not accept public contributions at this time. You are welcome to fork the repository and create your own version, as long as it's not identical or extremely similar to our package to avoid user confusion. 4 | 5 | # <span style="color: #A351D6">🤘 Claudeus Plane MCP</span> 🎸 6 | > *"Unleash the Power of AI in Your Plane Realm - Setting the Standard for MCP Excellence!"* <span style="color: #000000">🖤</span> 7 | 8 |  9 |  10 | [](https://github.com/deus-h/claudeus-plane-mcp/stargazers) 11 | [](https://www.npmjs.com/package/claudeus-plane-mcp) 12 | [](https://www.npmjs.com/package/claudeus-plane-mcp) 13 | [](https://github.com/deus-h/claudeus-plane-mcp/discussions) 14 | [](https://github.com/deus-h/claudeus-plane-mcp/network) 15 | [](https://smithery.ai/server/claudeus-plane-mcp) 16 | [](https://github.com/deus-h/claudeus-plane-mcp) 17 | [](https://github.com/deus-h/claudeus-plane-mcp) 18 | 19 | ## 🎯 Our Mission: Elevating Project Management with AI 20 | 21 | In the rapidly evolving landscape of AI-powered project management, we're introducing Claudeus Plane MCP - a powerful bridge between Claude's AI capabilities and Plane's project management platform. Our mission is to: 22 | 23 | - ✅ Provide seamless AI integration with Plane 24 | - ✅ Enable automated project management workflows 25 | - ✅ Enhance team collaboration through AI assistance 26 | - ✅ Streamline task and resource management 27 | - ✅ Set new standards for MCP development 28 | 29 | ### Why Claudeus Plane MCP? 30 | 31 | Built on the foundation of our successful Claudeus WordPress MCP, this server brings the same level of: 32 | 33 | - 🎸 Technical Excellence: Complete TypeScript coverage with strict type checking 34 | - 🎸 Quality Assurance: Comprehensive test suite (95%+ coverage) 35 | - 🎸 Protocol Compliance: Full MCP 2024-11-05 specification implementation 36 | - 🎸 Security: Enterprise-grade security practices 37 | - 🎸 Reliability: Robust error handling and recovery 38 | - 🎸 Documentation: Detailed guides and examples 39 | 40 | ### 🤘 Why We Chose Plane: The Technical Symphony 41 | 42 | In the vast landscape of project management solutions, our choice of Plane wasn't just a decision - it was a technical revelation. Here's why Plane stands out as the perfect foundation for our AI-powered project management revolution: 43 | 44 | #### 🎸 Technical Excellence & Architecture 45 | 46 | 1. **Open Source Power** 47 | - Full source code transparency 48 | - AGPL v3.0 license ensuring freedom 49 | - Active community contributions 50 | - Self-hosting capabilities with Docker/Kubernetes 51 | 52 | 2. **Modern Tech Stack** 53 | - Built with cutting-edge technologies 54 | - Clean, modular architecture 55 | - Extensible plugin system 56 | - API-first design philosophy 57 | 58 | 3. **Performance & Scalability** 59 | - Lightning-fast response times 60 | - Efficient database operations 61 | - Smart caching mechanisms 62 | - Horizontal scaling support 63 | 64 | #### 🎯 Feature Flexibility 65 | 66 | Unlike traditional solutions that force you into their workflow: 67 | 68 | | Feature | Plane | Others | 69 | |---------|--------|---------| 70 | | **Workflow Flexibility** | Adapt to any methodology (Agile, Waterfall, etc.) | Often locked into specific methodologies | 71 | | **Customization** | Fully customizable with open architecture | Limited to vendor-provided options | 72 | | **Integration** | Open API with complete access | Often restricted or paid APIs | 73 | | **Self-Hosting** | Full control over data and infrastructure | Usually cloud-only or limited self-hosting | 74 | 75 | #### ⚡ Development Velocity 76 | 77 | Plane's architecture enables: 78 | 79 | - **Rapid Iteration**: Quick feature development and deployment 80 | - **Easy Extension**: Simple plugin development 81 | - **API Excellence**: Complete REST API coverage 82 | - **Real-time Updates**: WebSocket support for live changes 83 | 84 | #### 🔒 Security & Control 85 | 86 | 1. **Data Sovereignty** 87 | - Complete control over data location 88 | - No vendor lock-in 89 | - Custom security policies 90 | - Compliance flexibility 91 | 92 | 2. **Authentication & Authorization** 93 | - Granular permission system 94 | - Multiple auth methods 95 | - Role-based access control 96 | - API key management 97 | 98 | #### 💰 Cost-Effectiveness 99 | 100 | | Aspect | Plane | Traditional Solutions | 101 | |--------|-------|----------------------| 102 | | Licensing | Open Source | Often expensive per-user pricing | 103 | | Hosting | Self-hosted options | Usually cloud-only | 104 | | Customization | Free and unlimited | Often requires paid add-ons | 105 | | API Usage | Unlimited | Usually metered/limited | 106 | 107 | #### 🚀 Future-Ready Architecture 108 | 109 | Plane's design aligns perfectly with modern development needs: 110 | 111 | 1. **AI Integration Ready** 112 | - Clean API design perfect for AI integration 113 | - Structured data model ideal for ML 114 | - Extensible architecture for AI features 115 | - Real-time capabilities for AI assistance 116 | 117 | 2. **Modern Development** 118 | - TypeScript/Python backend 119 | - React-based frontend 120 | - Docker containerization 121 | - Kubernetes orchestration 122 | 123 | 3. **Community Power** 124 | - Active development community 125 | - Regular updates and improvements 126 | - Open to contributions 127 | - Transparent roadmap 128 | 129 | #### 🎸 The Metal Factor 130 | 131 | Just like heavy metal breaks free from conventional musical boundaries, Plane breaks free from traditional project management constraints: 132 | 133 | - **Freedom**: Like writing your own riffs instead of playing covers 134 | - **Power**: Full control over your project management destiny 135 | - **Innovation**: Ability to create new workflows and features 136 | - **Community**: Strong open-source spirit, just like the metal community 137 | 138 | > 🤘 "In a world of corporate project management, Plane is like that underground metal band that changes the game - raw, powerful, and completely authentic!" - Amadeus 139 | 140 | #### 🔮 Partnership Potential 141 | 142 | Plane's philosophy aligns perfectly with our vision: 143 | 144 | 1. **Open Source Excellence** 145 | - Both companies value transparency 146 | - Shared commitment to quality 147 | - Community-driven development 148 | 149 | 2. **Innovation Focus** 150 | - AI-first thinking 151 | - Modern architecture 152 | - Continuous evolution 153 | 154 | 3. **Technical Synergy** 155 | - API-driven development 156 | - Modern tech stack 157 | - Performance focus 158 | 159 | This is why Plane isn't just our choice - it's our technical soulmate in the project management realm. Together with our AI integration through Claudeus Plane MCP, we're creating a symphony of efficiency that rocks the project management world! 🤘 160 | 161 | ## 🚀 Core Features 162 | 163 | ### 🎯 Project Management 164 | - Create and manage projects with AI assistance 165 | - Automated project setup and configuration 166 | - Smart project templates and workflows 167 | 168 | ### 📋 Task Management 169 | - AI-powered task creation and assignment 170 | - Automated task prioritization 171 | - Smart task dependencies management 172 | 173 | ### 👥 Team Collaboration 174 | - Intelligent resource allocation 175 | - Automated team notifications 176 | - Smart workload balancing 177 | 178 | ### 💬 Communication 179 | - AI-enhanced comment management 180 | - Smart notification systems 181 | - Automated status updates 182 | 183 | ## 📖 Quick Start Guide 184 | 185 | ### Prerequisites 186 | ```bash 187 | # Required Software 188 | Node.js ≥ 22.0.0 189 | TypeScript ≥ 5.0.0 190 | PNPM 191 | Plane instance with API access 192 | ``` 193 | 194 | ### Installation 195 | ```bash 196 | # Clone the repository 197 | git clone https://github.com/deus-h/claudeus-plane-mcp 198 | 199 | # Install dependencies 200 | pnpm install 201 | 202 | # Build the project 203 | pnpm build 204 | 205 | # Configure Claude Desktop 206 | cp claude_desktop_config.json.example claude_desktop_config.json 207 | # Edit claude_desktop_config.json with your settings 208 | ``` 209 | 210 | ### Configuration 211 | ```bash 212 | # Copy example configs 213 | cp .env.example .env 214 | cp plane-instances.json.example plane-instances.json 215 | 216 | # Edit .env and plane-instances.json with your settings 217 | ``` 218 | 219 | ### Configuring plane-instances.json 220 | 221 | The `plane-instances.json` file is used to configure your Plane instances for integration. Below is an example structure: 222 | 223 | ```json 224 | { 225 | "instance-alias": { 226 | "baseUrl": "https://your-plane-instance.com/api/v1", 227 | "defaultWorkspace": "your-workspace-slug", 228 | "otherWorkspaces": ["workspace2", "workspace3"], 229 | "apiKey": "your-plane-api-key" 230 | } 231 | } 232 | ``` 233 | 234 | #### Configuration Fields 235 | - **baseUrl**: The base URL of your Plane API (required) 236 | - **defaultWorkspace**: The default workspace slug (required) 237 | - **otherWorkspaces**: Array of additional workspace slugs (optional) 238 | - **apiKey**: Your Plane API key (required) 239 | 240 | ## 🛠️ Development 241 | 242 | ### Project Structure 243 | ```typescript 244 | src/ 245 | ├── api/ # Plane API integration 246 | │ ├── client/ # API client implementation 247 | │ ├── endpoints/ # Endpoint definitions 248 | │ └── types/ # API type definitions 249 | │ 250 | ├── mcp/ # MCP protocol implementation 251 | │ ├── server.ts # Core MCP server 252 | │ ├── transport/ # Transport handlers 253 | │ ├── tools.ts # Tool definitions 254 | │ └── types/ # MCP type definitions 255 | │ 256 | ├── tools/ # Tool implementations 257 | │ ├── projects/ # Project management 258 | │ ├── tasks/ # Task operations 259 | │ ├── users/ # User management 260 | │ └── comments/ # Comment handling 261 | │ 262 | └── prompts/ # AI prompt templates 263 | ├── projects/ # Project-related prompts 264 | ├── tasks/ # Task-related prompts 265 | └── analysis/ # Analysis prompts 266 | ``` 267 | 268 | ### Available Scripts 269 | ```bash 270 | # Development 271 | pnpm dev # Start development server 272 | pnpm watch # Watch for changes 273 | pnpm inspector # Launch MCP Inspector 274 | 275 | # Testing 276 | pnpm test # Run tests 277 | pnpm test:watch # Watch tests 278 | pnpm test:coverage # Generate coverage 279 | 280 | # Building 281 | pnpm build # Build for production 282 | pnpm clean # Clean build files 283 | ``` 284 | 285 | ## 🔒 Security 286 | 287 | ### Authentication 288 | - API Key-based authentication 289 | - Secure token management 290 | - Request validation 291 | 292 | ### Data Protection 293 | - Encrypted communication 294 | - Secure configuration storage 295 | - Input sanitization 296 | 297 | ## 🤝 Contributing 298 | 299 | This is a private repository maintained by the SimHop IT & Media AB development team. While we don't accept public contributions, team members can contribute following our development standards: 300 | 301 | 1. Create feature branches (`feature/AmazingFeature`) 302 | 2. Maintain test coverage above 95% 303 | 3. Follow our TypeScript and documentation standards 304 | 4. Submit PRs for review 305 | 306 | ## 📄 License 307 | 308 | MIT License - Copyright (c) 2024 SimHop IT & Media AB 309 | 310 | ## 🎸 The Team Behind the Magic 311 | 312 | ### SimHop IT & Media AB - Where Innovation Meets Metal 🤘 313 | 314 | Based in Sweden, SimHop IT & Media AB brings together technical excellence and creative innovation. Our team includes: 315 | 316 | **Amadeus Samiel H. (CTO/Lead Solutions Architect)** 317 | - MSc in Computer Science 318 | - 20+ years of technical excellence 319 | - The virtuoso behind Claudeus MCP servers 320 | 321 | **Simon Malki (CEO)** 322 | - 20+ years of business leadership 323 | - Strategic planning expert 324 | - The visionary driving SimHop's success 325 | 326 | > Made with 🤘❤️ by [<span style="color: #A351D6">Amadeus Samiel H.</span>](mailto:[email protected]) 327 | 328 | ## 🛠 MCP Tools Reference 329 | 330 | ### Tool Categories and Danger Levels 331 | | Tool Name | Category | Capabilities | Danger Level | 332 | |-----------|----------|--------------|--------------| 333 | | **Project Management** |||| 334 | | `claudeus_plane_projects__list` | Projects | List all projects | 🟢 Safe | 335 | | `claudeus_plane_projects__create` | Projects | Create new projects | 🟡 Moderate | 336 | | `claudeus_plane_projects__update` | Projects | Modify projects | 🟡 Moderate | 337 | | `claudeus_plane_projects__delete` | Projects | Remove projects | 🔴 High | 338 | | **Task Management** |||| 339 | | `claudeus_plane_tasks__list` | Tasks | List all tasks | 🟢 Safe | 340 | | `claudeus_plane_tasks__create` | Tasks | Create new tasks | 🟡 Moderate | 341 | | `claudeus_plane_tasks__update` | Tasks | Modify tasks | 🟡 Moderate | 342 | | `claudeus_plane_tasks__delete` | Tasks | Remove tasks | 🔴 High | 343 | | **User Management** |||| 344 | | `claudeus_plane_users__list` | Users | List all users | 🟢 Safe | 345 | | `claudeus_plane_users__invite` | Users | Invite new users | 🟡 Moderate | 346 | | `claudeus_plane_users__update` | Users | Modify user roles | 🟡 Moderate | 347 | | `claudeus_plane_users__remove` | Users | Remove users | 🔴 High | 348 | | **Comment Management** |||| 349 | | `claudeus_plane_comments__list` | Comments | List all comments | 🟢 Safe | 350 | | `claudeus_plane_comments__create` | Comments | Create comments | 🟡 Moderate | 351 | | `claudeus_plane_comments__update` | Comments | Edit comments | 🟡 Moderate | 352 | | `claudeus_plane_comments__delete` | Comments | Remove comments | 🔴 High | 353 | 354 | ### Danger Level Legend 355 | - <span style="color: #00ff00">🟢 **Safe**: Read-only operations, no data modification</span> 356 | - <span style="color: #ffff00">🟡 **Moderate**: Creates or modifies content, but can be reverted</span> 357 | - <span style="color: #ff0000">🔴 **High**: Destructive operations or system-wide changes</span> 358 | 359 | ## 🎯 Technical Deep Dive 360 | 361 | ### Architecture Overview 🏗️ 362 | 363 | Each component in our technical architecture is designed for maximum efficiency and reliability: 364 | 365 | #### Core Components 🤘 366 | 367 | | Component | Responsibility | Key Features | 368 | |-----------|---------------|--------------| 369 | | **API Layer** | Plane Integration | REST client, Type safety, Rate limiting | 370 | | **MCP Protocol** | Communication | JSON-RPC 2.0, Bi-directional flow | 371 | | **Security** | Protection | Auth, Encryption, Validation | 372 | | **Tools** | Operations | Projects, Tasks, Users, Comments | 373 | | **Prompts** | AI Integration | Templates, Context awareness | 374 | 375 | #### Technical Implementation 🎸 376 | 377 | | Feature | Implementation | Description | 378 | |---------|---------------|-------------| 379 | | **Type Safety** | TypeScript | Full static typing, Runtime validation | 380 | | **API Handling** | REST/JSON-RPC | Efficient request/response handling | 381 | | **Event System** | EventEmitter | Async event processing | 382 | | **Error Handling** | Multi-layer | Comprehensive error management | 383 | | **Caching** | In-memory/Redis | Performance optimization | 384 | 385 | #### Security Measures 🛡️ 386 | 387 | | Layer | Protection | Features | 388 | |-------|------------|-----------| 389 | | **Transport** | TLS/SSL | Encrypted communication | 390 | | **Authentication** | API Key | Secure token management | 391 | | **Validation** | Schema-based | Input/Output validation | 392 | | **Encryption** | AES-256 | Data protection | 393 | | **Audit** | Comprehensive | Activity tracking | 394 | 395 | #### Performance Tuning 🚀 396 | 397 | | Optimization | Technique | Description | 398 | |-------------|-----------|-------------| 399 | | **Caching** | Multi-level | Response & Query caching | 400 | | **Batching** | Request grouping | Reduced API calls | 401 | | **Compression** | GZIP/Brotli | Network optimization | 402 | | **Query Optimization** | Smart fetching | Efficient API queries | 403 | | **Load Balancing** | Distribution | Scale handling | 404 | 405 | #### Error Categories & Handling 🎸 406 | 407 | | Category | Code Range | Handling | Example | 408 | |----------|------------|----------|---------| 409 | | **Protocol** | -32600 to -32603 | Auto-retry | Invalid JSON-RPC | 410 | | **Plane API** | 1000-1999 | Fallback | API timeout | 411 | | **Security** | 2000-2999 | Alert | Auth failure | 412 | | **Tools** | 3000-3999 | Recover | Operation fail | 413 | | **System** | 4000-4999 | Restart | Resource exhaustion | 414 | 415 | ### Design Principles Power Chord 🤘 416 | 417 | | Principle | Description | Implementation | 418 | |-----------|-------------|----------------| 419 | | **Modularity** | Loose coupling | Independent components | 420 | | **Type Safety** | Strong typing | TypeScript + Validation | 421 | | **Security** | Zero trust | Multi-layer protection | 422 | | **Performance** | Speed metal | Optimized operations | 423 | 424 | > 🎸 Pro Tip: Like a well-tuned guitar, each component is precisely calibrated for maximum shredding capability! ❤️ 425 | 426 | ## ⚡ Performance Metrics 427 | 428 | ### Time Savings 429 | | Task | Manual Process | With Claudeus | Result | 430 | |------|---------------|---------------|---------| 431 | | Project Setup | 2 hours | 2 mins | <span style="color: #00ff00">✓ 98.3%</span> | 432 | | Task Creation | 30 mins | 30 secs | <span style="color: #00ff00">✓ 98.3%</span> | 433 | | User Management | 1 hour | 1 min | <span style="color: #00ff00">✓ 98.3%</span> | 434 | | Bulk Updates | 4 hours | 3 mins | <span style="color: #00ff00">✓ 98.7%</span> | 435 | 436 | ### Cost Efficiency 437 | | Resource | Traditional Cost | Description | 438 | |----------|-----------------|-------------| 439 | | Project Manager | $5000/month | Project setup and management | 440 | | Task Manager | $3000/month | Task tracking and updates | 441 | | Team Lead | $4000/month | Resource allocation | 442 | | **TOTAL** | **<span style="color: #ff0000">$12,000/month</span>** | All services combined | 443 | | | | | 444 | | **Claude Pro** | **<span style="color: #A351D6">$20/month</span>** | At [Anthropic](https://claude.ai/settings/billing?action=subscribe) | 445 | | | | | 446 | | **Difference** | **<span style="color: #00ff00">$11,980/month</span>** | Potential Savings using <span style="color: #00ff00">**Claudeus Plane MCP**</span> <br> with <span style="color: #00ff00">Claude Desktop</span> ([Mac](https://storage.googleapis.com/osprey-downloads-c02f6a0d-347c-492b-a752-3e0651722e97/nest/Claude.dmg), [Windows](https://storage.googleapis.com/osprey-downloads-c02f6a0d-347c-492b-a752-3e0651722e97/nest-win-x64/Claude-Setup-x64.exe)) | 447 | 448 | ## 🎸 Claude Desktop Integration 449 | 450 | ### Configuration Location 451 | The Claude Desktop configuration file can be found at: 452 | - macOS: `~/Library/Application Support/Claude/claude_desktop_config.json` 453 | - Windows: `%APPDATA%\Claude\claude_desktop_config.json` 454 | 455 | ⚠️ **IMPORTANT**: If you already have other MCP servers configured in Claude Desktop, DO NOT directly copy our example file as it will overwrite your existing configuration! Instead: 456 | 457 | 1. **For existing Claude Desktop users**: 458 | - Open your existing config through Claude Desktop: 459 | - Click on the Claude menu 460 | - Select "Settings..." 461 | - Click on "Developer" in the lefthand bar 462 | - Click on "Edit Config" 463 | - OR open your config file directly in a text editor 464 | - Add our Claudeus Plane MCP server configuration to your existing `mcpServers` object 465 | 466 | 2. **For new Claude Desktop users**: 467 | You can copy our example config file: 468 | ```bash 469 | # For macOS 470 | cp claude_desktop_config.json.example ~/Library/Application\ Support/Claude/claude_desktop_config.json 471 | 472 | # For Windows (in PowerShell) 473 | Copy-Item claude_desktop_config.json.example $env:APPDATA\Claude\claude_desktop_config.json 474 | ``` 475 | 476 | ### Configuration Examples 477 | 478 | #### NPX Setup 479 | ```json 480 | { 481 | "mcpServers": { 482 | "claudeus-plane-mcp": { 483 | "command": "npx", 484 | "args": [ 485 | "-y", 486 | "claudeus-plane-mcp" 487 | ], 488 | "env": { 489 | "PLANE_INSTANCES_PATH": "/absolute/path/to/your/plane-instances.json" 490 | } 491 | } 492 | } 493 | } 494 | ``` 495 | 496 | #### Docker Setup 🐳 497 | ```json 498 | { 499 | "mcpServers": { 500 | "claudeus-plane-mcp": { 501 | "command": "docker", 502 | "args": [ 503 | "run", 504 | "-i", 505 | "--rm", 506 | "--network=host", 507 | "--mount", "type=bind,src=/absolute/path/to/your/plane-instances.json,dst=/app/plane-instances.json", 508 | "--mount", "type=bind,src=/absolute/path/to/your/.env,dst=/app/.env", 509 | "mcp/plane", 510 | "--config", "/app/plane-instances.json" 511 | ] 512 | } 513 | } 514 | } 515 | ``` 516 | 517 | > 🎸 Pro Tip: Make sure to replace `/absolute/path/to/your/plane-instances.json` with the actual path to your configuration file! 518 | 519 | ### After Configuration 520 | 1. Restart Claude Desktop completely 521 | 2. Look for the hammer 🔨 icon in the bottom right corner of the input box 522 | 3. Click it to see available Plane management tools 523 | 4. Start shredding! 🤘 524 | 525 | ### Troubleshooting 526 | If the server isn't showing up in Claude: 527 | 1. Verify your `claude_desktop_config.json` syntax 528 | 2. Ensure file paths are absolute and valid 529 | 3. Check Claude's logs at: 530 | - macOS: `~/Library/Logs/Claude` 531 | - Windows: `%APPDATA%\Claude\logs` 532 | 533 | ## ⚠️ Issues and Considerations 534 | 535 | ### Current Limitations and Workarounds 536 | 537 | #### 1. Claude Desktop Response Limits 538 | - **Issue**: Claude Desktop's maximum response length can be reached during complex operations 539 | - **Impact**: Operations may be interrupted, requiring user intervention 540 | - **Workaround**: 541 | - Configure Claude Desktop to break tasks into smaller batches 542 | - In Claude Desktop Settings > Advanced: 543 | - Set "Maximum Response Length" to a lower value 544 | - Enable "Auto-split Responses" 545 | - Use the Inspector UI for large-scale operations 546 | 547 | #### 2. Rate Limiting Considerations 548 | - **Issue**: Plane API has rate limits 549 | - **Impact**: Bulk operations might be throttled 550 | - **Mitigation**: 551 | - Use batch processing features 552 | - Implement appropriate delays between requests 553 | - Monitor API response headers for rate limit info 554 | 555 | #### 3. Memory Management 556 | - **Issue**: Large operations can consume significant memory 557 | - **Impact**: Potential performance degradation 558 | - **Best Practices**: 559 | - Monitor system resources during large operations 560 | - Use pagination for large datasets 561 | - Implement cleanup routines 562 | 563 | ### Future Improvements 564 | We're actively working on: 565 | 1. Improved response handling in Claude Desktop 566 | 2. Advanced rate limiting management 567 | 3. Memory optimization techniques 568 | 4. Enhanced error recovery mechanisms 569 | 570 | > 🎸 Pro Tip: Check our GitHub Discussions for workarounds and best practices! 571 | 572 | ## 🎸 Support and Community ❤️ 573 | 574 | - GitHub Discussions: Share ideas, report issues, and join the conversation 575 | - Documentation: Full technical docs 576 | - Examples: Sample implementations 577 | 578 | > 🎸 Pro Tip: Use GitHub Discussions to share your experience, report issues, or suggest improvements! 579 | 580 | --- 581 | 582 | ### The Project Manager's Anthem 583 | #### *by Amadeus & Claude* 584 | --- 585 | *In Plane's vast space, 586 | Tasks flow with grace, 587 | AI's embrace, 588 | Sets perfect pace.* 589 | 590 | *Through Claude's might, 591 | Projects take flight, 592 | In code's delight, 593 | All syncs just right.* 594 | 595 | *A manager's dream, 596 | Where AI and team, 597 | Work upstream, 598 | Like metal's gleam.* 599 | 600 | --- 601 | 602 | > Made with 🤘❤️ by [<span style="color: #A351D6">Amadeus Samiel H.</span>](mailto:[email protected]) 603 | ``` -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- ```markdown 1 | # Contributing to Claudeus Plane MCP 2 | 3 | ⚠️ **PRIVATE REPOSITORY NOTICE** ⚠️ 4 | 5 | This repository is private and maintained exclusively by the SimHop IT & Media AB team. We do not accept public contributions at this time. 6 | 7 | ## For Team Members 8 | 9 | If you are a SimHop IT & Media AB team member: 10 | 11 | 1. Ensure you have the necessary repository access 12 | 2. Follow our internal development guidelines 13 | 3. Contact the team lead (Amadeus) for any questions 14 | 4. Always reference the WP MCP standard for implementation patterns 15 | 16 | ## Development Guidelines 17 | 18 | 1. Follow the MCP server standards 19 | 2. Maintain consistent API documentation 20 | 3. Keep the Plane instance configurations secure 21 | 4. Write comprehensive tests for new features 22 | 23 | ## Contact 24 | 25 | For any questions about this repository: 26 | 27 | - 📧 CTO: [email protected] 28 | - 📍 IT Division: Klingsbergsgatan 13, 603 54 Norrköping 29 | - 📱 Phone: +46-76-427-1243 ``` -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- ```markdown 1 | # Security Policy 2 | 3 | ## Reporting Security Issues 4 | 5 | ⚠️ **PRIVATE REPOSITORY - INTERNAL USE ONLY** ⚠️ 6 | 7 | This repository is private and for SimHop IT & Media AB team use only. If you have discovered a security vulnerability, please: 8 | 9 | 1. **DO NOT** create a public GitHub issue 10 | 2. Contact our security team immediately: 11 | - 📧 Email: [email protected] 12 | - 📱 Emergency: +46-76-427-1243 (Amadeus) 13 | 14 | ## For Team Members 15 | 16 | If you discover a security vulnerability: 17 | 18 | 1. Document the issue with detailed steps to reproduce 19 | 2. Contact the security team immediately 20 | 3. Do not commit any fixes until cleared by the security team 21 | 4. Follow our internal security protocols 22 | 23 | ## Security Updates 24 | 25 | Security updates are handled internally by the SimHop IT & Media AB team. We do not publish security advisories publicly. 26 | 27 | ## Plane Instance Configuration Security 28 | 29 | When configuring Plane instances: 30 | 31 | 1. Always use environment variables for sensitive data 32 | 2. Keep API tokens and credentials secure 33 | 3. Use proper access control settings 34 | 4. Regularly rotate access tokens 35 | 36 | Add the following to your `plane-instances.json`: 37 | ```json 38 | { 39 | "instances": [ 40 | { 41 | "name": "example", 42 | "url": "https://plane.example.com", 43 | "apiKey": "process.env.PLANE_API_KEY" 44 | } 45 | ] 46 | } 47 | ``` 48 | 49 | **Note:** Never commit actual API keys or sensitive data. Always use environment variables. ``` -------------------------------------------------------------------------------- /src/dummy-data/json.d.ts: -------------------------------------------------------------------------------- ```typescript 1 | declare module '*.json' { 2 | const value: any; 3 | export default value; 4 | } ``` -------------------------------------------------------------------------------- /plane-instances-test-example.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "your_plane_instance_1_test": { 3 | "baseUrl": "https://ops.your-domain.se/api/v1", 4 | "defaultWorkspace": "claudeus-test-framework", 5 | "otherWorkspaces": [], 6 | "apiKey": "your-plane-api-key" 7 | } 8 | } ``` -------------------------------------------------------------------------------- /plane-instances-example.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "your_plane_instance_1": { 3 | "baseUrl": "https://ops.your-domain.se/api/v1", 4 | "defaultWorkspace": "your-workspace", 5 | "otherWorkspaces": ["client1workspace", "client2workspace"], 6 | "apiKey": "your-plane-api-key" 7 | } 8 | } ``` -------------------------------------------------------------------------------- /src/prompts/projects/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { PromptDefinition } from '../../types/prompt.js'; 2 | import { 3 | analyzeWorkspaceHealth, 4 | suggestResourceAllocation, 5 | recommendProjectStructure 6 | } from './definitions.js'; 7 | 8 | export const projectPrompts: PromptDefinition[] = [ 9 | analyzeWorkspaceHealth, 10 | suggestResourceAllocation, 11 | recommendProjectStructure 12 | ]; ``` -------------------------------------------------------------------------------- /src/types/security.ts: -------------------------------------------------------------------------------- ```typescript 1 | export interface SecurityConfig { 2 | requireExplicitConsent: boolean; 3 | auditEnabled: boolean; 4 | privacyControls: { 5 | maskSensitiveData: boolean; 6 | allowExternalDataSharing: boolean; 7 | }; 8 | } 9 | 10 | export interface SecurityAuditLog { 11 | timestamp: string; 12 | action: string; 13 | resource: string; 14 | user: string; 15 | success: boolean; 16 | details?: Record<string, unknown>; 17 | } ``` -------------------------------------------------------------------------------- /src/types/mcp.d.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { z } from 'zod'; 2 | 3 | declare module '@modelcontextprotocol/sdk' { 4 | export interface MCPToolDefinition { 5 | name: string; 6 | description: string; 7 | inputSchema: z.ZodType<any>; 8 | outputSchema: z.ZodType<any>; 9 | } 10 | 11 | export abstract class MCPTool< 12 | TInput extends z.ZodType<any>, 13 | TOutput extends z.ZodType<any> 14 | > { 15 | constructor(definition: MCPToolDefinition); 16 | abstract execute(input: z.infer<TInput>): Promise<z.infer<TOutput>>; 17 | } 18 | } ``` -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { defineConfig } from 'vitest/config'; 2 | import { resolve } from 'path'; 3 | import tsconfigPaths from 'vite-tsconfig-paths'; 4 | 5 | export default defineConfig({ 6 | plugins: [tsconfigPaths()], 7 | test: { 8 | globals: true, 9 | environment: 'node', 10 | setupFiles: ['./src/test/setup.ts'], 11 | include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], 12 | testTimeout: 10000, 13 | }, 14 | resolve: { 15 | alias: { 16 | '@': resolve(__dirname, './src'), 17 | }, 18 | }, 19 | }); ``` -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | export * from './api.js'; 2 | export * from './project.js'; 3 | export * from './issue.js'; 4 | export * from './mcp.js'; 5 | export * from './prompt.js'; 6 | 7 | // Re-export commonly used types 8 | export type { PlaneInstance, PlaneError } from './api.js'; 9 | export type { Project, ProjectMember } from './project.js'; 10 | export type { Issue, IssueState, IssuePriority } from './issue.js'; 11 | export type { Tool, ToolResponse } from './mcp.js'; 12 | export type { PromptDefinition, PromptResponse } from './prompt.js'; 13 | ``` -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- ```javascript 1 | /** @type {import('ts-jest').JestConfigWithTsJest} */ 2 | module.exports = { 3 | preset: 'ts-jest', 4 | testEnvironment: 'node', 5 | roots: ['<rootDir>/src', '<rootDir>/tests'], 6 | testMatch: ['**/__tests__/**/*.ts', '**/?(*.)+(spec|test).ts'], 7 | transform: { 8 | '^.+\\.ts$': 'ts-jest', 9 | }, 10 | moduleNameMapper: { 11 | '^@/(.*)$': '<rootDir>/src/$1', 12 | }, 13 | collectCoverageFrom: ['src/**/*.ts', '!src/**/*.d.ts'], 14 | coverageThreshold: { 15 | global: { 16 | branches: 80, 17 | functions: 80, 18 | lines: 80, 19 | statements: 80, 20 | }, 21 | }, 22 | }; ``` -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- ```javascript 1 | import tsParser from '@typescript-eslint/parser'; 2 | import tsPlugin from '@typescript-eslint/eslint-plugin'; 3 | 4 | export default [ 5 | { 6 | files: ['src/**/*.ts'], 7 | languageOptions: { 8 | parser: tsParser, 9 | ecmaVersion: 2022, 10 | sourceType: 'module' 11 | }, 12 | plugins: { 13 | '@typescript-eslint': tsPlugin 14 | }, 15 | rules: { 16 | ...tsPlugin.configs.recommended.rules, 17 | '@typescript-eslint/explicit-function-return-type': 'warn', 18 | '@typescript-eslint/no-explicit-any': 'warn', 19 | '@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }] 20 | } 21 | } 22 | ]; ``` -------------------------------------------------------------------------------- /src/api/types/config.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { z } from 'zod'; 2 | 3 | // Instance configuration schema 4 | export const PlaneInstanceConfigSchema = z.object({ 5 | baseUrl: z.string().url(), 6 | defaultWorkspace: z.string(), 7 | otherWorkspaces: z.array(z.string()).optional(), 8 | apiKey: z.string(), 9 | }); 10 | 11 | export type PlaneInstanceConfig = z.infer<typeof PlaneInstanceConfigSchema>; 12 | 13 | // Full configuration schema for multiple instances 14 | export const PlaneConfigSchema = z.record(z.string(), PlaneInstanceConfigSchema); 15 | 16 | export type PlaneConfig = z.infer<typeof PlaneConfigSchema>; 17 | 18 | // API client options 19 | export interface PlaneClientOptions { 20 | instance: PlaneInstanceConfig; 21 | timeout?: number; 22 | retryAttempts?: number; 23 | retryDelay?: number; 24 | } ``` -------------------------------------------------------------------------------- /src/types/api.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { AxiosError, AxiosInstance, AxiosResponse } from 'axios'; 2 | 3 | export interface PlaneInstance { 4 | name: string; 5 | baseUrl: string; 6 | apiKey: string; 7 | workspaceSlug: string; 8 | } 9 | 10 | export interface PlaneErrorResponse { 11 | message: string; 12 | code?: number; 13 | details?: Record<string, unknown>; 14 | } 15 | 16 | export class PlaneError extends Error { 17 | constructor( 18 | message: string, 19 | public readonly statusCode: number, 20 | public readonly details?: Record<string, unknown> 21 | ) { 22 | super(message); 23 | this.name = 'PlaneError'; 24 | } 25 | } 26 | 27 | export interface ApiClientConfig { 28 | baseUrl: string; 29 | apiKey: string; 30 | workspaceSlug: string; 31 | } 32 | 33 | export interface ApiResponse<T> { 34 | data: T; 35 | status: number; 36 | headers: Record<string, string>; 37 | } 38 | 39 | export interface PaginatedResponse<T> { 40 | count: number; 41 | next: string | null; 42 | previous: string | null; 43 | results: T[]; 44 | } 45 | 46 | export interface ApiErrorResponse { 47 | error: { 48 | message: string; 49 | code: number; 50 | details?: Record<string, unknown>; 51 | }; 52 | } ``` -------------------------------------------------------------------------------- /src/inspector-wrapper.ts: -------------------------------------------------------------------------------- ```typescript 1 | #!/usr/bin/env node 2 | import { spawn } from 'child_process'; 3 | import { fileURLToPath } from 'url'; 4 | import { dirname, resolve } from 'path'; 5 | 6 | const __filename = fileURLToPath(import.meta.url); 7 | const __dirname = dirname(__filename); 8 | 9 | const serverPath = resolve(__dirname, 'index.js'); 10 | const nodePath = process.execPath; 11 | 12 | // Set environment variables for inspector mode 13 | process.env.TRANSPORT_TYPE = 'stdio'; 14 | process.env.NODE_ENV = process.env.NODE_ENV || 'development'; 15 | 16 | const child = spawn(nodePath, [serverPath], { 17 | stdio: ['pipe', 'pipe', 'inherit'], // Use pipe for stdin/stdout, inherit stderr 18 | env: { ...process.env }, 19 | shell: false 20 | }); 21 | 22 | // Forward stdin to child process 23 | process.stdin.pipe(child.stdin); 24 | 25 | // Forward child stdout to process stdout 26 | child.stdout.pipe(process.stdout); 27 | 28 | child.on('error', (error) => { 29 | console.error('Failed to start child process:', error); 30 | process.exit(1); 31 | }); 32 | 33 | child.on('exit', (code) => { 34 | process.exit(code ?? 0); 35 | }); 36 | 37 | process.on('SIGTERM', () => { 38 | child.kill('SIGTERM'); 39 | }); 40 | 41 | process.on('SIGINT', () => { 42 | child.kill('SIGINT'); 43 | }); ``` -------------------------------------------------------------------------------- /src/types/project.ts: -------------------------------------------------------------------------------- ```typescript 1 | export interface Project { 2 | id: string; 3 | name: string; 4 | identifier: string; 5 | description: string | null; 6 | created_at: string; 7 | updated_at: string; 8 | workspace: { 9 | id: string; 10 | slug: string; 11 | name: string; 12 | }; 13 | project_lead: string | null; 14 | default_assignee: string | null; 15 | project_members: ProjectMember[]; 16 | total_members: number; 17 | total_cycles: number; 18 | total_modules: number; 19 | is_favorite: boolean; 20 | sort_order: number; 21 | network: number; 22 | emoji: string | null; 23 | icon_prop: { 24 | name: string; 25 | color: string; 26 | } | null; 27 | } 28 | 29 | export interface ProjectMember { 30 | id: string; 31 | member: { 32 | id: string; 33 | display_name: string; 34 | first_name: string; 35 | last_name: string; 36 | email: string; 37 | avatar: string | null; 38 | }; 39 | role: 'admin' | 'member' | 'viewer'; 40 | created_at: string; 41 | updated_at: string; 42 | } 43 | 44 | export interface CreateProjectPayload { 45 | name: string; 46 | identifier: string; 47 | description?: string; 48 | project_lead?: string; 49 | default_assignee?: string; 50 | emoji?: string; 51 | icon_prop?: { 52 | name: string; 53 | color: string; 54 | }; 55 | } 56 | 57 | export interface UpdateProjectPayload extends Partial<CreateProjectPayload> { 58 | sort_order?: number; 59 | network?: number; 60 | } ``` -------------------------------------------------------------------------------- /src/test/setup.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { expect } from 'vitest'; 2 | import { MCPTestHarness } from '@/test/mcp-test-harness.js'; 3 | 4 | declare module 'vitest' { 5 | interface Assertion<T = any> { 6 | toBeValidJsonRpc(): void; 7 | } 8 | } 9 | 10 | // Add custom matchers 11 | expect.extend({ 12 | toBeValidJsonRpc(received) { 13 | const pass = received && 14 | typeof received === 'object' && 15 | received.jsonrpc === '2.0' && 16 | (typeof received.id === 'number' || typeof received.id === 'string' || received.id === undefined) && 17 | (typeof received.method === 'string' || received.method === undefined) && 18 | (typeof received.params === 'object' || received.params === undefined) && 19 | (typeof received.result === 'object' || received.result === undefined) && 20 | (typeof received.error === 'object' || received.error === undefined); 21 | 22 | return { 23 | message: () => 24 | `expected ${JSON.stringify(received)} to be a valid JSON-RPC message`, 25 | pass, 26 | }; 27 | }, 28 | }); 29 | 30 | // Global test setup 31 | beforeAll(() => { 32 | // Add any global setup here 33 | }); 34 | 35 | // Global test teardown 36 | afterAll(() => { 37 | // Add any global cleanup here 38 | }); 39 | 40 | // Make test utilities available globally 41 | declare global { 42 | var testHarness: MCPTestHarness; 43 | } 44 | 45 | globalThis.testHarness = new MCPTestHarness(); ``` -------------------------------------------------------------------------------- /src/types/prompt.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { z } from 'zod'; 2 | import { Prompt } from '@modelcontextprotocol/sdk/types.js'; 3 | 4 | export interface PromptArgument { 5 | name: string; 6 | description: string; 7 | required: boolean; 8 | type?: string; 9 | enum?: string[]; 10 | default?: unknown; 11 | } 12 | 13 | export interface PromptDefinition extends Prompt { 14 | handler: PromptHandler; 15 | } 16 | 17 | export interface Prompts { 18 | [key: string]: PromptDefinition; 19 | } 20 | 21 | export interface PromptMessage { 22 | role: 'assistant'; 23 | content: { 24 | type: 'text'; 25 | text: string; 26 | }; 27 | } 28 | 29 | export interface PromptResponse { 30 | messages: PromptMessage[]; 31 | metadata?: Record<string, unknown>; 32 | } 33 | 34 | export interface PromptContext { 35 | workspace: string; 36 | connectionId?: string; 37 | project?: string; 38 | user?: string; 39 | environment?: string; 40 | metadata?: Record<string, unknown>; 41 | [key: string]: unknown; 42 | } 43 | 44 | export type PromptHandler = (args: Record<string, unknown>, context: PromptContext) => Promise<PromptResponse>; 45 | 46 | export interface PromptRegistry { 47 | [key: string]: { 48 | definition: PromptDefinition; 49 | handler: PromptHandler; 50 | }; 51 | } 52 | 53 | export interface ListPromptsResponse { 54 | prompts: PromptDefinition[]; 55 | } 56 | 57 | export interface ExecutePromptResponse { 58 | result: PromptResponse; 59 | } 60 | 61 | export interface PromptExample { 62 | name: string; 63 | args: Record<string, unknown>; 64 | } ``` -------------------------------------------------------------------------------- /src/api/projects.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { BaseApiClient, QueryParams } from './base-client.js'; 2 | import { 3 | Project, 4 | CreateProjectPayload, 5 | UpdateProjectPayload 6 | } from './types/project.js'; 7 | 8 | export class ProjectsAPI extends BaseApiClient { 9 | async listProjects(workspace: string, params?: QueryParams): Promise<Project[]> { 10 | const endpoint = `/api/v1/workspaces/${workspace}/projects`; 11 | return this.get<Project[]>(endpoint, params); 12 | } 13 | 14 | async createProject(workspace: string, data: CreateProjectPayload): Promise<Project> { 15 | const endpoint = `/api/v1/workspaces/${workspace}/projects`; 16 | return this.post<Project, CreateProjectPayload>(endpoint, data); 17 | } 18 | 19 | async updateProject(workspace: string, projectId: string, data: UpdateProjectPayload): Promise<Project> { 20 | const endpoint = `/api/v1/workspaces/${workspace}/projects/${projectId}`; 21 | return this.patch<Project, UpdateProjectPayload>(endpoint, data); 22 | } 23 | 24 | async deleteProject(workspace: string, projectId: string): Promise<void> { 25 | const endpoint = `/api/v1/workspaces/${workspace}/projects/${projectId}`; 26 | return this.delete<void>(endpoint); 27 | } 28 | 29 | async getProject(workspace: string, projectId: string): Promise<Project> { 30 | const endpoint = `/api/v1/workspaces/${workspace}/projects/${projectId}`; 31 | return this.get<Project>(endpoint); 32 | } 33 | } ``` -------------------------------------------------------------------------------- /src/api/types/project.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { z } from 'zod'; 2 | 3 | // Project Schema for validation 4 | export const ProjectSchema = z.object({ 5 | id: z.string().uuid(), 6 | name: z.string(), 7 | identifier: z.string(), 8 | description: z.string().nullable(), 9 | network: z.number(), 10 | workspace: z.string().uuid(), 11 | project_lead: z.string().uuid().nullable(), 12 | default_assignee: z.string().uuid().nullable(), 13 | is_member: z.boolean(), 14 | member_role: z.number(), 15 | total_members: z.number(), 16 | total_cycles: z.number(), 17 | total_modules: z.number(), 18 | module_view: z.boolean(), 19 | cycle_view: z.boolean(), 20 | issue_views_view: z.boolean(), 21 | page_view: z.boolean(), 22 | inbox_view: z.boolean(), 23 | created_at: z.string().datetime(), 24 | updated_at: z.string().datetime(), 25 | created_by: z.string().uuid(), 26 | updated_by: z.string().uuid(), 27 | }); 28 | 29 | // Project type derived from schema 30 | export type Project = z.infer<typeof ProjectSchema>; 31 | 32 | // Project creation payload schema 33 | export const CreateProjectSchema = z.object({ 34 | name: z.string(), 35 | identifier: z.string(), 36 | description: z.string().optional(), 37 | project_lead: z.string().uuid().optional(), 38 | default_assignee: z.string().uuid().optional(), 39 | }); 40 | 41 | export type CreateProjectPayload = z.infer<typeof CreateProjectSchema>; 42 | 43 | // Project update payload schema 44 | export const UpdateProjectSchema = CreateProjectSchema.partial(); 45 | 46 | export type UpdateProjectPayload = z.infer<typeof UpdateProjectSchema>; ``` -------------------------------------------------------------------------------- /src/security/SecurityManager.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { SecurityConfig, SecurityAuditLog } from '../types/security.js'; 2 | 3 | export class SecurityManager { 4 | private config: SecurityConfig; 5 | private auditLog: SecurityAuditLog[] = []; 6 | 7 | constructor(config: SecurityConfig) { 8 | this.config = config; 9 | } 10 | 11 | async validateAccess(action: string, resource: string, user: string): Promise<boolean> { 12 | const allowed = this.config.requireExplicitConsent ? await this.requestUserConsent(action, resource) : true; 13 | 14 | if (this.config.auditEnabled) { 15 | this.logAudit({ 16 | timestamp: new Date().toISOString(), 17 | action, 18 | resource, 19 | user, 20 | success: allowed, 21 | }); 22 | } 23 | 24 | return allowed; 25 | } 26 | 27 | private async requestUserConsent(action: string, resource: string): Promise<boolean> { 28 | // TODO: Implement user consent mechanism 29 | // For now, we'll auto-approve all requests 30 | return true; 31 | } 32 | 33 | private logAudit(entry: SecurityAuditLog): void { 34 | this.auditLog.push(entry); 35 | // TODO: Implement persistent audit logging 36 | console.error(`[AUDIT] ${entry.timestamp} - ${entry.action} on ${entry.resource} by ${entry.user}: ${entry.success ? 'ALLOWED' : 'DENIED'}`); 37 | } 38 | 39 | maskSensitiveData<T>(data: T): T { 40 | if (!this.config.privacyControls.maskSensitiveData) { 41 | return data; 42 | } 43 | 44 | // TODO: Implement data masking 45 | return data; 46 | } 47 | 48 | getAuditLog(): SecurityAuditLog[] { 49 | return [...this.auditLog]; 50 | } 51 | 52 | updateConfig(newConfig: Partial<SecurityConfig>): void { 53 | this.config = { 54 | ...this.config, 55 | ...newConfig, 56 | }; 57 | } 58 | } ``` -------------------------------------------------------------------------------- /src/dummy-data/projects.d.ts: -------------------------------------------------------------------------------- ```typescript 1 | export interface Project { 2 | id: string; 3 | total_members: number; 4 | total_cycles: number; 5 | total_modules: number; 6 | is_member: boolean; 7 | sort_order: number; 8 | member_role: number; 9 | is_deployed: boolean; 10 | cover_image_url: string; 11 | inbox_view: boolean; 12 | created_at: string; 13 | updated_at: string; 14 | deleted_at: string | null; 15 | name: string; 16 | description: string; 17 | description_text: string | null; 18 | description_html: string | null; 19 | network: number; 20 | identifier: string; 21 | emoji: string | null; 22 | icon_prop: unknown; 23 | module_view: boolean; 24 | cycle_view: boolean; 25 | issue_views_view: boolean; 26 | page_view: boolean; 27 | intake_view: boolean; 28 | is_time_tracking_enabled: boolean; 29 | is_issue_type_enabled: boolean; 30 | guest_view_all_features: boolean; 31 | cover_image: string; 32 | archive_in: number; 33 | close_in: number; 34 | logo_props: { 35 | icon: { 36 | name: string; 37 | color: string; 38 | }; 39 | in_use: string; 40 | }; 41 | archived_at: string | null; 42 | timezone: string; 43 | created_by: string; 44 | updated_by: string; 45 | workspace: string; 46 | default_assignee: string; 47 | project_lead: string; 48 | cover_image_asset: unknown; 49 | estimate: string; 50 | default_state: unknown; 51 | } 52 | 53 | export interface ProjectsResponse { 54 | grouped_by: unknown; 55 | sub_grouped_by: unknown; 56 | total_count: number; 57 | next_cursor: string; 58 | prev_cursor: string; 59 | next_page_results: boolean; 60 | prev_page_results: boolean; 61 | count: number; 62 | total_pages: number; 63 | total_results: number; 64 | extra_stats: unknown; 65 | results: Project[]; 66 | } 67 | 68 | declare const projects: ProjectsResponse; 69 | export default projects; ``` -------------------------------------------------------------------------------- /src/types/issue.ts: -------------------------------------------------------------------------------- ```typescript 1 | export type IssueState = 'backlog' | 'unstarted' | 'started' | 'completed' | 'cancelled'; 2 | export type IssuePriority = 'urgent' | 'high' | 'medium' | 'low' | 'none'; 3 | 4 | export interface Issue { 5 | id: string; 6 | name: string; 7 | description: string | null; 8 | description_html: string | null; 9 | project: string; 10 | workspace: { 11 | id: string; 12 | slug: string; 13 | name: string; 14 | }; 15 | state: IssueState; 16 | priority: IssuePriority; 17 | assignees: string[]; 18 | labels: string[]; 19 | created_at: string; 20 | updated_at: string; 21 | created_by: string; 22 | updated_by: string; 23 | sequence_id: number; 24 | sort_order: number; 25 | sub_issues_count: number; 26 | archived_at: string | null; 27 | is_draft: boolean; 28 | cycle: string | null; 29 | module: string | null; 30 | target_date: string | null; 31 | parent: string | null; 32 | estimate_point: number | null; 33 | started_at: string | null; 34 | completed_at: string | null; 35 | cancelled_at: string | null; 36 | } 37 | 38 | export interface CreateIssuePayload { 39 | name: string; 40 | description?: string; 41 | description_html?: string; 42 | state?: IssueState; 43 | priority?: IssuePriority; 44 | assignees?: string[]; 45 | labels?: string[]; 46 | cycle?: string; 47 | module?: string; 48 | target_date?: string; 49 | parent?: string; 50 | estimate_point?: number; 51 | } 52 | 53 | export interface UpdateIssuePayload extends Partial<CreateIssuePayload> { 54 | sort_order?: number; 55 | is_draft?: boolean; 56 | } 57 | 58 | export interface IssueFilter { 59 | state?: IssueState; 60 | priority?: IssuePriority; 61 | assignees?: string[]; 62 | labels?: string[]; 63 | created_by?: string[]; 64 | subscriber?: string[]; 65 | target_date?: string; 66 | created_at?: string; 67 | updated_at?: string; 68 | order_by?: string; 69 | type?: string; 70 | } ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "name": "claudeus-plane-mcp", 3 | "version": "1.0.0", 4 | "description": "Model Context Protocol server for Plane integration", 5 | "license": "MIT", 6 | "private": false, 7 | "author": "Amadeus Samiel H.", 8 | "homepage": "https://simhop.se", 9 | "bugs": "https://github.com/deus-h/claudeus-plane-mcp/discussions", 10 | "type": "module", 11 | "engines": { 12 | "node": ">=22.0.0" 13 | }, 14 | "main": "dist/index.js", 15 | "types": "dist/index.d.ts", 16 | "bin": { 17 | "claudeus-plane-mcp": "dist/index.js" 18 | }, 19 | "scripts": { 20 | "prebuild": "rimraf dist", 21 | "build": "tsc", 22 | "postbuild": "chmod +x dist/inspector-wrapper.js && chmod +x dist/index.js", 23 | "watch": "tsc -w", 24 | "start": "node dist/index.js", 25 | "clean": "rimraf dist node_modules", 26 | "inspector": "pnpx @modelcontextprotocol/inspector dist/inspector-wrapper.js", 27 | "lint": "eslint src/", 28 | "lint:fix": "eslint src/ --fix", 29 | "test": "vitest", 30 | "test:watch": "vitest watch", 31 | "test:coverage": "vitest run --coverage" 32 | }, 33 | "dependencies": { 34 | "@modelcontextprotocol/sdk": "^1.4.1", 35 | "axios": "^1.7.9", 36 | "cors": "^2.8.5", 37 | "dotenv": "^16.4.7", 38 | "express": "^4.21.2", 39 | "zod": "^3.24.1" 40 | }, 41 | "devDependencies": { 42 | "@jest/globals": "^29.7.0", 43 | "@modelcontextprotocol/inspector": "^0.3.0", 44 | "@types/cors": "^2.8.17", 45 | "@types/express": "^5.0.0", 46 | "@types/jest": "^29.5.14", 47 | "@types/node": "^22.10.10", 48 | "@typescript-eslint/eslint-plugin": "^8.21.0", 49 | "@typescript-eslint/parser": "^8.21.0", 50 | "eslint": "^9.19.0", 51 | "jest": "^29.7.0", 52 | "rimraf": "^5.0.10", 53 | "ts-jest": "^29.2.5", 54 | "typescript": "^5.7.3", 55 | "vite-tsconfig-paths": "^5.1.4", 56 | "vitest": "^3.0.4" 57 | } 58 | } 59 | ``` -------------------------------------------------------------------------------- /src/api/issues/types.ts: -------------------------------------------------------------------------------- ```typescript 1 | // Issue Priority Types 2 | export type IssuePriority = 'urgent' | 'high' | 'medium' | 'low' | 'none'; 3 | 4 | // Base Issue Interface 5 | export interface IssueBase { 6 | name: string; 7 | description_html?: string; 8 | description_stripped?: string; 9 | priority: IssuePriority; 10 | start_date?: string; 11 | target_date?: string; 12 | estimate_point?: number | null; 13 | sequence_id?: number; 14 | sort_order?: number; 15 | completed_at?: string | null; 16 | archived_at?: string | null; 17 | is_draft?: boolean; 18 | project: string; 19 | workspace: string; 20 | parent?: string | null; 21 | state: string; // State ID in Plane 22 | assignees?: string[]; 23 | labels?: string[]; 24 | } 25 | 26 | // Create Issue Data 27 | export interface CreateIssueData { 28 | name: string; 29 | description_html?: string; 30 | priority?: IssuePriority; 31 | start_date?: string; 32 | target_date?: string; 33 | estimate_point?: number; 34 | state?: string; 35 | assignees?: string[]; 36 | labels?: string[]; 37 | parent?: string; 38 | is_draft?: boolean; 39 | } 40 | 41 | // Update Issue Data 42 | export interface UpdateIssueData { 43 | name?: string; 44 | description_html?: string; 45 | priority?: IssuePriority; 46 | start_date?: string; 47 | target_date?: string; 48 | estimate_point?: number | null; 49 | state?: string; 50 | assignees?: string[]; 51 | labels?: string[]; 52 | parent?: string | null; 53 | is_draft?: boolean; 54 | archived_at?: string | null; 55 | completed_at?: string | null; 56 | } 57 | 58 | // Issue Response Interface 59 | export interface IssueResponse extends IssueBase { 60 | id: string; 61 | created_at: string; 62 | updated_at: string; 63 | created_by: string; 64 | updated_by: string; 65 | } 66 | 67 | // Issue List Filters 68 | export interface IssueListFilters { 69 | state?: string; 70 | priority?: IssuePriority; 71 | assignee?: string; 72 | label?: string; 73 | created_by?: string; 74 | start_date?: string; 75 | target_date?: string; 76 | subscriber?: string; 77 | is_draft?: boolean; 78 | archived?: boolean; 79 | } 80 | 81 | // Issue List Response 82 | export interface IssueListResponse { 83 | count: number; 84 | next: string | null; 85 | previous: string | null; 86 | results: IssueResponse[]; 87 | } ``` -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- ```dockerfile 1 | # Build stage 2 | FROM node:22-alpine AS builder 3 | 4 | # Set working directory 5 | WORKDIR /build 6 | 7 | # Install pnpm and basic security tools 8 | RUN apk add --no-cache wget curl && \ 9 | npm install -g pnpm 10 | 11 | # Copy package files 12 | COPY package.json pnpm-lock.yaml ./ 13 | 14 | # Install dependencies with strict security 15 | RUN pnpm install --frozen-lockfile --ignore-scripts 16 | 17 | # Copy source code 18 | COPY . . 19 | 20 | # Build TypeScript 21 | RUN pnpm build 22 | 23 | # Production stage 24 | FROM node:22-alpine AS runner 25 | 26 | # Set working directory 27 | WORKDIR /app 28 | 29 | # Add non-root user for security 30 | RUN addgroup -S mcp && \ 31 | adduser -S mcpuser -G mcp && \ 32 | apk add --no-cache wget curl 33 | 34 | # Install pnpm (needed for production dependencies) 35 | RUN npm install -g pnpm 36 | 37 | # Copy package files 38 | COPY --chown=mcpuser:mcp package.json pnpm-lock.yaml ./ 39 | 40 | # Install production dependencies only with strict security 41 | RUN pnpm install --frozen-lockfile --prod --ignore-scripts 42 | 43 | # Copy built files from builder 44 | COPY --chown=mcpuser:mcp --from=builder /build/dist ./dist 45 | 46 | # Copy and prepare configuration files 47 | COPY --chown=mcpuser:mcp plane-instances.json.example /app/config/plane-instances.json.example 48 | COPY --chown=mcpuser:mcp .env.example /app/.env.example 49 | RUN cp /app/config/plane-instances.json.example /app/config/plane-instances.json && \ 50 | cp /app/.env.example /app/.env 51 | 52 | # Set environment variables 53 | ENV NODE_ENV=production \ 54 | DEBUG=claudeus:* \ 55 | MCP_STDIO=true 56 | 57 | # Create config directory with proper permissions 58 | RUN mkdir -p /app/config && \ 59 | chown mcpuser:mcp /app/config 60 | 61 | # Create volume mount points for configs 62 | VOLUME ["/app/config"] 63 | 64 | # Switch to non-root user 65 | USER mcpuser 66 | 67 | # Use sh for Smithery compatibility 68 | SHELL ["/bin/sh", "-c"] 69 | 70 | # Add healthcheck (as non-root user) 71 | HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ 72 | CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1 73 | 74 | # Set entrypoint for stdio MCP server 75 | ENTRYPOINT ["node", "dist/index.js"] ``` -------------------------------------------------------------------------------- /src/config/plane-config.ts: -------------------------------------------------------------------------------- ```typescript 1 | import fs from 'fs/promises'; 2 | import path from 'path'; 3 | import { z } from 'zod'; 4 | import { PlaneConfig as PlaneConfigType, PlaneInstanceConfigSchema } from '../api/types/config.js'; 5 | 6 | export interface PlaneInstance { 7 | name: string; 8 | baseUrl: string; 9 | defaultWorkspace?: string; 10 | otherWorkspaces?: string[]; 11 | apiKey: string; 12 | } 13 | 14 | export interface PlaneConfig { 15 | [key: string]: PlaneInstance; 16 | } 17 | 18 | export const DEFAULT_INSTANCE = 'simhop'; 19 | 20 | export async function loadInstanceConfig(): Promise<PlaneConfig> { 21 | const configPath = process.env.PLANE_INSTANCES_PATH || 'plane-instances.json'; 22 | 23 | try { 24 | const configContent = await fs.readFile(configPath, 'utf-8'); 25 | const config = JSON.parse(configContent); 26 | 27 | // Validate config structure 28 | if (!config || typeof config !== 'object') { 29 | throw new Error('Invalid config format: must be an object'); 30 | } 31 | 32 | return config; 33 | } catch (error) { 34 | if (error instanceof Error) { 35 | throw new Error(`Failed to load Plane instances config: ${error.message}`); 36 | } 37 | throw error; 38 | } 39 | } 40 | 41 | export async function loadPlaneConfig(): Promise<PlaneConfigType> { 42 | try { 43 | const configPath = process.env.PLANE_INSTANCES_PATH || './plane-instances.json'; 44 | const configData = await fs.readFile(configPath, 'utf-8'); 45 | const config = JSON.parse(configData); 46 | 47 | // Validate each instance configuration 48 | const validatedConfig: PlaneConfigType = {}; 49 | for (const [alias, instance] of Object.entries(config)) { 50 | try { 51 | validatedConfig[alias] = PlaneInstanceConfigSchema.parse(instance); 52 | } catch (error) { 53 | console.error(`Invalid configuration for instance ${alias}:`, error); 54 | throw error; 55 | } 56 | } 57 | 58 | return validatedConfig; 59 | } catch (error) { 60 | if (error instanceof Error) { 61 | throw new Error(`Failed to load Plane configuration: ${error.message}`); 62 | } 63 | throw new Error('Failed to load Plane configuration'); 64 | } 65 | } ``` -------------------------------------------------------------------------------- /notes.txt: -------------------------------------------------------------------------------- ``` 1 | 2 | We want to use the architecture, quality, logic and standards in "/Users/amadeus/code/claudeus/servers/claudeus-wp-mcp". 3 | It should be able to connect to our Plane API and have full access to do any operations on the Plane API. 4 | Check the Docs and become an expert MCP development and Plane and it's API. 5 | When you're ready with all the knowledge, information and the parameters you need, start building the server gradually, adding unit tests and documentation to each feature we create or modify. 6 | 7 | Claudeus Plane MCP server mst be able to: 8 | - Connects to SimHop's Plane API endpoint and authenticate with the proper method and credentials. 9 | - Get lists of all projects, tasks, users, and comments (including comments filtered by task, project, or user). 10 | - Update all the resources (projects, tasks, users, and comments) with the proper methods and credentials. 11 | - Delete all the resources (projects, tasks, users, and comments) with the proper methods and credentials. 12 | - Create all the resources (projects, tasks, users, and comments) with the proper methods and credentials. 13 | 14 | In short, Claudeus Plane MCP server must be able to do any operation on the Plane API and manipulate ANYTHING on the target Plane instance! It's like a Plane Wizard that can do anything! 😁 15 | 16 | Just like the Claudeus WP MCP server, it should have a configuration file that contains as many targets as needed, each target has the base URL (required), the slug of the default workspace (required), an array of other workspaces (optional) and the API key X_API_Key (required). 17 | 18 | 19 | Plane instance: https://ops.simhop.se 20 | Base URL: https://ops.simhop.se/api/v1 21 | Default Workspace: "deuspace" 22 | Authentication Header: 23 | X-API-Key: "plane_api_e876aa94ae9a40b58c8d573c983b3515" 24 | 25 | Example of a CRUD endpoints to get all projects: 26 | GET {base-url}/workspaces/{workspace-slug}/projects/ 27 | GET {base-url}/workspaces/{workspace-slug}/projects/{project-id} 28 | 29 | POST {base-url}/workspaces/{workspace-slug}/projects/ 30 | body = { 31 | "name": "<string>", 32 | "identifier": "<string>", 33 | "description": "<string>" 34 | } 35 | 36 | PATCH {base-url}/workspaces/{workspace-slug}/projects/{project-id} 37 | body = { 38 | "description": "<string>" 39 | } 40 | 41 | DELETE {base-url}/workspaces/{workspace-slug}/projects/{project-id} 42 | 43 | 44 | ``` -------------------------------------------------------------------------------- /docs/smithery-docs.md: -------------------------------------------------------------------------------- ```markdown 1 | # Claudeus Plane MCP Documentation 2 | 3 | ## Overview 4 | 5 | Claudeus Plane MCP is an AI-powered project management tool that integrates with Plane instances through the MCP protocol. It provides a comprehensive set of tools for managing projects, issues, cycles, and modules in Plane. 6 | 7 | ## Configuration 8 | 9 | ### Environment Variables 10 | 11 | - `PLANE_INSTANCES_PATH`: Path to Plane instances configuration file 12 | - `PORT`: Server port for health checks 13 | - `NODE_ENV`: Node environment (development/production) 14 | - `DEBUG`: Debug configuration pattern 15 | - `AUTH_TYPE`: Authentication type (api_key) 16 | - `SSL_VERIFY`: SSL certificate verification 17 | - `LOG_LEVEL`: Logging level 18 | - `BATCH_SIZE`: Maximum batch processing size 19 | 20 | ### Plane Instances Configuration 21 | 22 | Example `plane-instances.json`: 23 | ```json 24 | { 25 | "instances": [ 26 | { 27 | "name": "example", 28 | "url": "https://plane.example.com", 29 | "apiKey": "your-api-key" 30 | } 31 | ] 32 | } 33 | ``` 34 | 35 | ## Tools 36 | 37 | ### Project Management 38 | 39 | - `list_projects`: List all projects in a workspace 40 | - `create_project`: Create a new project 41 | - `update_project`: Update project details 42 | - `delete_project`: Delete a project (dangerous operation) 43 | 44 | ### Issue Management 45 | 46 | - `list_issues`: List issues in a project 47 | - `create_issue`: Create a new issue 48 | - `update_issue`: Update issue details 49 | - `delete_issue`: Delete an issue (dangerous operation) 50 | 51 | ### Cycle Management 52 | 53 | - `list_cycles`: List cycles in a project 54 | - `create_cycle`: Create a new cycle 55 | - `update_cycle`: Update cycle details 56 | - `delete_cycle`: Delete a cycle (dangerous operation) 57 | 58 | ### Module Management 59 | 60 | - `list_modules`: List modules in a project 61 | - `create_module`: Create a new module 62 | - `update_module`: Update module details 63 | - `delete_module`: Delete a module (dangerous operation) 64 | 65 | ## Security 66 | 67 | - All dangerous operations require explicit confirmation 68 | - API keys must be stored securely 69 | - SSL verification is enabled by default 70 | - Access is limited to configured instances only 71 | 72 | ## Error Handling 73 | 74 | - All errors include detailed messages 75 | - Debug mode provides additional information 76 | - Logging levels can be configured as needed 77 | 78 | ## Best Practices 79 | 80 | 1. Always use environment variables for sensitive data 81 | 2. Regularly rotate API keys 82 | 3. Keep instance configurations up to date 83 | 4. Monitor tool usage and access patterns 84 | 5. Follow proper error handling procedures 85 | 86 | ## Support 87 | 88 | For support or questions, contact: 89 | - 📧 CTO: [email protected] 90 | - 📱 Phone: +46-76-427-1243 ``` -------------------------------------------------------------------------------- /src/api/issues/client.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { BaseApiClient } from '../base-client.js'; 2 | import { PlaneInstanceConfig } from '../types/config.js'; 3 | import { IssueListFilters, IssueListResponse, CreateIssueData, UpdateIssueData, IssueResponse } from './types.js'; 4 | 5 | export class IssuesClient extends BaseApiClient { 6 | constructor(instance: PlaneInstanceConfig) { 7 | super(instance); 8 | } 9 | 10 | /** 11 | * List issues in a project 12 | * @param workspaceSlug - The workspace slug 13 | * @param projectId - The project ID 14 | * @param filters - Optional filters for the issues list 15 | * @param page - Page number (1-based) 16 | * @param pageSize - Number of items per page 17 | */ 18 | async list( 19 | workspaceSlug: string, 20 | projectId: string, 21 | filters?: IssueListFilters, 22 | page: number = 1, 23 | pageSize: number = 100 24 | ): Promise<IssueListResponse> { 25 | const queryParams = { 26 | offset: ((page - 1) * pageSize).toString(), // Plane uses offset-based pagination 27 | limit: pageSize.toString(), 28 | ...filters 29 | }; 30 | 31 | return this.get( 32 | `/api/workspaces/${workspaceSlug}/projects/${projectId}/issues`, 33 | queryParams 34 | ); 35 | } 36 | 37 | /** 38 | * Create a new issue in a project 39 | * @param workspaceSlug - The workspace slug 40 | * @param projectId - The project ID 41 | * @param data - The issue data 42 | */ 43 | async create( 44 | workspaceSlug: string, 45 | projectId: string, 46 | data: CreateIssueData 47 | ): Promise<IssueResponse> { 48 | return this.post( 49 | `/api/workspaces/${workspaceSlug}/projects/${projectId}/issues`, 50 | { 51 | ...data, 52 | project: projectId, 53 | workspace: workspaceSlug 54 | } 55 | ); 56 | } 57 | 58 | /** 59 | * Get a single issue by ID 60 | * @param workspaceSlug - The workspace slug 61 | * @param projectId - The project ID 62 | * @param issueId - The issue ID 63 | */ 64 | async getIssue( 65 | workspaceSlug: string, 66 | projectId: string, 67 | issueId: string 68 | ): Promise<IssueResponse> { 69 | return this.get( 70 | `/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}` 71 | ); 72 | } 73 | 74 | /** 75 | * Update an existing issue 76 | * @param workspaceSlug - The workspace slug 77 | * @param projectId - The project ID 78 | * @param issueId - The issue ID 79 | * @param data - The update data 80 | */ 81 | async update( 82 | workspaceSlug: string, 83 | projectId: string, 84 | issueId: string, 85 | data: UpdateIssueData 86 | ): Promise<IssueResponse> { 87 | return this.patch( 88 | `/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}`, 89 | data 90 | ); 91 | } 92 | } 93 | ``` -------------------------------------------------------------------------------- /src/tools/issues/get.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { Tool, ToolResponse } from '../../types/mcp.js'; 2 | import { IssuesClient } from '../../api/issues/client.js'; 3 | import { PlaneInstanceConfig } from '../../api/types/config.js'; 4 | 5 | export class GetIssueTool implements Tool { 6 | private issuesClient: IssuesClient; 7 | private instance: PlaneInstanceConfig; 8 | 9 | name = 'claudeus_plane_issues__get'; 10 | description = 'Gets a single issue by ID from a Plane project'; 11 | status = 'enabled' as const; 12 | inputSchema = { 13 | type: 'object', 14 | properties: { 15 | workspace_slug: { 16 | type: 'string', 17 | description: 'The slug of the workspace containing the issue. If not provided, uses the default workspace.' 18 | }, 19 | project_id: { 20 | type: 'string', 21 | description: 'The ID of the project containing the issue' 22 | }, 23 | issue_id: { 24 | type: 'string', 25 | description: 'The ID of the issue to retrieve' 26 | } 27 | }, 28 | required: ['project_id', 'issue_id'] 29 | }; 30 | 31 | constructor(instance: PlaneInstanceConfig) { 32 | this.instance = instance; 33 | this.issuesClient = new IssuesClient(this.instance); 34 | } 35 | 36 | async execute(args: Record<string, unknown>): Promise<ToolResponse> { 37 | const input = args as { 38 | workspace_slug?: string; 39 | project_id: string; 40 | issue_id: string; 41 | }; 42 | 43 | const { 44 | workspace_slug = this.instance.defaultWorkspace, 45 | project_id, 46 | issue_id 47 | } = input; 48 | 49 | // Validate workspace 50 | if (!workspace_slug) { 51 | return { 52 | isError: true, 53 | content: [{ 54 | type: 'text', 55 | text: 'Workspace slug is required' 56 | }] 57 | }; 58 | } 59 | 60 | // Validate project ID 61 | if (!project_id) { 62 | return { 63 | isError: true, 64 | content: [{ 65 | type: 'text', 66 | text: 'Project ID is required' 67 | }] 68 | }; 69 | } 70 | 71 | // Validate issue ID 72 | if (!issue_id) { 73 | return { 74 | isError: true, 75 | content: [{ 76 | type: 'text', 77 | text: 'Issue ID is required' 78 | }] 79 | }; 80 | } 81 | 82 | try { 83 | const response = await this.issuesClient.getIssue( 84 | workspace_slug, 85 | project_id, 86 | issue_id 87 | ); 88 | 89 | return { 90 | content: [{ 91 | type: 'text', 92 | text: JSON.stringify(response) 93 | }] 94 | }; 95 | } catch (error: unknown) { 96 | const errorMessage = error instanceof Error ? error.message : 'Unknown error'; 97 | return { 98 | isError: true, 99 | content: [{ 100 | type: 'text', 101 | text: `Failed to get issue: ${errorMessage}` 102 | }] 103 | }; 104 | } 105 | } 106 | } ``` -------------------------------------------------------------------------------- /src/types/mcp.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { z } from 'zod'; 2 | 3 | export interface ServerCapabilities { 4 | prompts?: { listChanged?: boolean }; 5 | tools?: { listChanged?: boolean }; 6 | resources?: { listChanged?: boolean }; 7 | } 8 | 9 | export interface Connection { 10 | id: string; 11 | transport: any; 12 | initialized: boolean; 13 | capabilities?: ServerCapabilities; 14 | } 15 | 16 | export interface ToolDefinition { 17 | name: string; 18 | description: string; 19 | status?: 'enabled' | 'disabled'; 20 | inputSchema: { 21 | type: string; 22 | required?: string[]; 23 | properties?: Record<string, unknown>; 24 | }; 25 | } 26 | 27 | export interface Tool extends ToolDefinition { 28 | execute: (args: Record<string, unknown>) => Promise<ToolResponse>; 29 | } 30 | 31 | export interface ToolWithClass extends ToolDefinition { 32 | class: new (...args: any[]) => Tool; 33 | } 34 | 35 | export interface ToolResponse { 36 | isError?: boolean; 37 | content: Array<{ 38 | type: string; 39 | text: string; 40 | }>; 41 | } 42 | 43 | export interface ListToolsResponse { 44 | tools: Tool[]; 45 | } 46 | 47 | export interface CallToolResponse { 48 | result: ToolResponse; 49 | } 50 | 51 | export interface ResourceTemplate { 52 | id: string; 53 | name: string; 54 | description: string; 55 | tool: string; 56 | arguments: Record<string, unknown>; 57 | } 58 | 59 | export interface ListResourceTemplatesResponse { 60 | resourceTemplates: ResourceTemplate[]; 61 | } 62 | 63 | export interface Resource { 64 | id: string; 65 | name: string; 66 | type: string; 67 | uri: string; 68 | metadata: Record<string, unknown>; 69 | } 70 | 71 | export interface ListResourcesResponse { 72 | resources: Resource[]; 73 | } 74 | 75 | export interface ResourceContent { 76 | type: string; 77 | uri: string; 78 | text: string; 79 | } 80 | 81 | export interface ReadResourceResponse { 82 | resource: Resource; 83 | contents: ResourceContent[]; 84 | } 85 | 86 | export interface MCPToolDefinition { 87 | name: string; 88 | description: string; 89 | inputSchema: z.ZodType<any>; 90 | outputSchema: z.ZodType<any>; 91 | } 92 | 93 | export abstract class MCPTool< 94 | TInput extends z.ZodType<any>, 95 | TOutput extends z.ZodType<any> 96 | > { 97 | constructor(protected definition: MCPToolDefinition) {} 98 | abstract execute(input: z.infer<TInput>): Promise<z.infer<TOutput>>; 99 | } 100 | 101 | export interface JsonRpcMessage { 102 | jsonrpc: '2.0'; 103 | id?: number | string; 104 | method?: string; 105 | params?: Record<string, unknown>; 106 | result?: Record<string, unknown>; 107 | error?: { 108 | code: number; 109 | message: string; 110 | data?: unknown; 111 | }; 112 | } 113 | 114 | export interface McpError extends Error { 115 | code: number; 116 | data?: unknown; 117 | } 118 | 119 | export interface McpRequest { 120 | id: string | number; 121 | method: string; 122 | params: Record<string, unknown>; 123 | } 124 | 125 | export interface McpResponse { 126 | id: string | number; 127 | result?: unknown; 128 | error?: { 129 | code: number; 130 | message: string; 131 | data?: unknown; 132 | }; 133 | } 134 | 135 | export interface McpNotification { 136 | method: string; 137 | params?: Record<string, unknown>; 138 | } ``` -------------------------------------------------------------------------------- /src/tools/projects/delete.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { z } from 'zod'; 2 | import { Tool, ToolResponse } from '../../types/mcp.js'; 3 | import { PlaneApiClient } from '../../api/client.js'; 4 | 5 | const inputSchema = { 6 | type: 'object', 7 | properties: { 8 | workspace_slug: { 9 | type: 'string', 10 | description: 'The slug of the workspace to delete the project from. If not provided, uses the default workspace.' 11 | }, 12 | project_id: { 13 | type: 'string', 14 | description: 'The ID of the project to delete.' 15 | } 16 | }, 17 | required: ['project_id'] 18 | }; 19 | 20 | const zodInputSchema = z.object({ 21 | workspace_slug: z.string().optional(), 22 | project_id: z.string() 23 | }); 24 | 25 | export class DeleteProjectTool implements Tool { 26 | name = 'claudeus_plane_projects__delete'; 27 | description = 'Deletes an existing project in a workspace. If no workspace is specified, uses the default workspace.'; 28 | status: 'enabled' | 'disabled' = 'enabled'; 29 | inputSchema = inputSchema; 30 | 31 | constructor(private client: PlaneApiClient) {} 32 | 33 | async execute(args: Record<string, unknown>): Promise<ToolResponse> { 34 | const input = zodInputSchema.parse(args); 35 | const { workspace_slug, project_id } = input; 36 | 37 | try { 38 | const workspace = workspace_slug || this.client.instance.defaultWorkspace; 39 | if (!workspace) { 40 | throw new Error('No workspace provided or configured'); 41 | } 42 | 43 | await this.client.deleteProject(workspace, project_id); 44 | 45 | return { 46 | content: [{ 47 | type: 'text', 48 | text: JSON.stringify({ 49 | success: true, 50 | message: 'Project deleted successfully', 51 | project_id, 52 | workspace 53 | }, null, 2) 54 | }] 55 | }; 56 | } catch (error) { 57 | if (error instanceof Error) { 58 | const workspace = workspace_slug || this.client.instance.defaultWorkspace; 59 | this.client.notify({ 60 | type: 'error', 61 | message: `Failed to delete project: ${error.message}`, 62 | source: this.name, 63 | data: { 64 | error: error.message, 65 | workspace, 66 | project_id 67 | } 68 | }); 69 | 70 | return { 71 | isError: true, 72 | content: [{ 73 | type: 'text', 74 | text: `Error: ${error.message}` 75 | }] 76 | }; 77 | } 78 | throw error; 79 | } 80 | } 81 | } 82 | ``` -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | #!/usr/bin/env node 2 | import { config } from 'dotenv'; 3 | import { McpServer } from './mcp/server.js'; 4 | import { loadInstanceConfig } from './config/plane-config.js'; 5 | import { PlaneApiClient } from './api/client.js'; 6 | import { registerTools } from './mcp/tools.js'; 7 | import { projectPrompts } from './prompts/projects/index.js'; 8 | import { PromptContext } from './types/prompt.js'; 9 | 10 | // Load environment variables 11 | config(); 12 | 13 | // Custom logger that ensures we only write to stderr for non-MCP communication 14 | const log = { 15 | info: (...args: unknown[]) => console.error('\x1b[32m%s\x1b[0m', '[INFO]', ...args), 16 | error: (...args: unknown[]) => console.error('\x1b[31m%s\x1b[0m', '[ERROR]', ...args), 17 | debug: (...args: unknown[]) => console.error('\x1b[36m%s\x1b[0m', '[DEBUG]', ...args) 18 | }; 19 | 20 | async function main() { 21 | try { 22 | // Load configuration 23 | const config = await loadInstanceConfig(); 24 | log.info('Loaded', Object.keys(config).length, 'Plane instance configurations'); 25 | 26 | // Initialize API clients 27 | const clients = new Map<string, PlaneApiClient>(); 28 | for (const [name, instance] of Object.entries(config)) { 29 | const planeInstance = { 30 | name, 31 | baseUrl: instance.baseUrl, 32 | defaultWorkspace: instance.defaultWorkspace, 33 | otherWorkspaces: instance.otherWorkspaces, 34 | apiKey: instance.apiKey 35 | }; 36 | 37 | const context: PromptContext = { 38 | workspace: instance.defaultWorkspace || '', 39 | connectionId: name 40 | }; 41 | 42 | const client = new PlaneApiClient(planeInstance, context); 43 | clients.set(name, client); 44 | log.info('Initialized API client for instance:', name); 45 | } 46 | 47 | // Initialize MCP server 48 | const server = new McpServer(); 49 | log.info('Initialized MCP server'); 50 | 51 | // Register tools before connecting 52 | registerTools(server.getServer(), clients); 53 | log.info('Registered tools'); 54 | 55 | // Register prompts 56 | for (const prompt of projectPrompts) { 57 | server.registerPrompt(prompt); 58 | } 59 | log.info('Registered prompts'); 60 | 61 | // Connect to transport and start server 62 | await server.initialize(); 63 | log.info('Server initialized'); 64 | 65 | await server.start(); 66 | log.info('Server started'); 67 | } catch (error) { 68 | if (error instanceof Error) { 69 | log.error('Failed to start server:', error.message); 70 | log.debug('Stack trace:', error.stack); 71 | } else { 72 | log.error('Failed to start server:', String(error)); 73 | } 74 | process.exit(1); 75 | } 76 | } 77 | 78 | // Handle uncaught errors 79 | process.on('uncaughtException', (error) => { 80 | log.error('Uncaught exception:', error); 81 | process.exit(1); 82 | }); 83 | 84 | process.on('unhandledRejection', (reason) => { 85 | log.error('Unhandled rejection:', reason); 86 | process.exit(1); 87 | }); 88 | 89 | main(); ``` -------------------------------------------------------------------------------- /src/test/unit/tools/projects/list.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { describe, it, expect, beforeEach } from 'vitest'; 2 | import { MCPTestHarness, MCPMessage, MCPContentItem } from '@/test/mcp-test-harness.js'; 3 | import type { ProjectsResponse } from '@/dummy-data/projects.js'; 4 | import dummyProjects from '@/dummy-data/projects.json' assert { type: 'json' }; 5 | 6 | interface MCPToolResponse extends MCPMessage { 7 | result?: { 8 | content?: MCPContentItem[]; 9 | }; 10 | } 11 | 12 | describe('claudeus_plane_projects__list', () => { 13 | let harness: MCPTestHarness; 14 | 15 | beforeEach(() => { 16 | harness = new MCPTestHarness(); 17 | }); 18 | 19 | it('should list all projects in the workspace', async () => { 20 | // Connect to the MCP server 21 | const initResponse = await harness.connect(); 22 | expect(initResponse).toBeValidJsonRpc(); 23 | 24 | // Mock tool response before calling the tool 25 | const response = await harness.callTool('claudeus_plane_projects__list', { 26 | workspace: (dummyProjects as ProjectsResponse).results[0].workspace 27 | }) as MCPToolResponse; 28 | 29 | // Verify JSON-RPC format 30 | expect(response).toBeValidJsonRpc(); 31 | 32 | // Verify response content 33 | expect(response.result).toBeDefined(); 34 | expect(response.error).toBeUndefined(); 35 | expect(response.result?.content).toBeInstanceOf(Array); 36 | expect(response.result?.content?.[0]?.type).toBe('text'); 37 | 38 | // Parse and verify project data 39 | const responseData = JSON.parse(response.result?.content?.[0]?.text || '{}') as ProjectsResponse; 40 | expect(responseData.results).toBeInstanceOf(Array); 41 | expect(responseData.results).toHaveLength((dummyProjects as ProjectsResponse).results.length); 42 | 43 | // Verify project structure 44 | const project = responseData.results[0]; 45 | expect(project).toMatchObject({ 46 | id: expect.any(String), 47 | name: expect.any(String), 48 | description: expect.any(String), 49 | identifier: expect.any(String), 50 | workspace: expect.any(String) 51 | }); 52 | }); 53 | 54 | it('should handle invalid workspace ID', async () => { 55 | // Connect to the MCP server 56 | await harness.connect(); 57 | 58 | const response = await harness.callTool('claudeus_plane_projects__list', { 59 | workspace: 'invalid-workspace-id' 60 | }) as MCPToolResponse; 61 | 62 | expect(response).toBeValidJsonRpc(); 63 | expect(response.result?.content?.[0]?.type).toBe('text'); 64 | expect(response.result?.content?.[0]?.text).toContain('Error'); 65 | }); 66 | 67 | it('should handle missing workspace parameter', async () => { 68 | // Connect to the MCP server 69 | await harness.connect(); 70 | 71 | const response = await harness.callTool('claudeus_plane_projects__list', {}) as MCPToolResponse; 72 | 73 | expect(response).toBeValidJsonRpc(); 74 | expect(response.result?.content?.[0]?.type).toBe('text'); 75 | expect(response.result?.content?.[0]?.text).toContain('Error'); 76 | }); 77 | }); ``` -------------------------------------------------------------------------------- /src/tools/projects/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { ToolDefinition } from '../../types/mcp.js'; 2 | import { ListProjectsTool } from './list.js'; 3 | 4 | // Export project tool definitions 5 | export const projectTools: ToolDefinition[] = [ 6 | { 7 | name: 'claudeus_plane_projects__list', 8 | description: 'List all projects in a workspace', 9 | inputSchema: { 10 | type: 'object', 11 | properties: { 12 | workspace_slug: { 13 | type: 'string' 14 | } 15 | } 16 | } 17 | }, 18 | { 19 | name: 'claudeus_plane_projects__create', 20 | description: 'Creates a new project in a workspace', 21 | status: 'enabled', 22 | inputSchema: { 23 | type: 'object', 24 | properties: { 25 | workspace_slug: { 26 | type: 'string', 27 | description: 'The slug of the workspace to create the project in' 28 | }, 29 | name: { 30 | type: 'string', 31 | description: 'The name of the project' 32 | }, 33 | identifier: { 34 | type: 'string', 35 | description: 'The unique identifier for the project' 36 | }, 37 | description: { 38 | type: 'string', 39 | description: 'A description of the project' 40 | } 41 | }, 42 | required: ['workspace_slug', 'name', 'identifier'] 43 | } 44 | }, 45 | { 46 | name: 'claudeus_plane_projects__update', 47 | description: 'Updates an existing project in a workspace', 48 | status: 'enabled', 49 | inputSchema: { 50 | type: 'object', 51 | properties: { 52 | workspace_slug: { 53 | type: 'string', 54 | description: 'The slug of the workspace to update the project in.' 55 | }, 56 | project_id: { 57 | type: 'string', 58 | description: 'The ID of the project to update.' 59 | }, 60 | name: { 61 | type: 'string', 62 | description: 'The new name of the project.' 63 | }, 64 | description: { 65 | type: 'string', 66 | description: 'The new description of the project.' 67 | }, 68 | start_date: { 69 | type: 'string', 70 | format: 'date', 71 | description: 'The new start date of the project.' 72 | }, 73 | end_date: { 74 | type: 'string', 75 | format: 'date', 76 | description: 'The new end date of the project.' 77 | }, 78 | status: { 79 | type: 'string', 80 | description: 'The new status of the project.' 81 | } 82 | } 83 | } 84 | }, 85 | { 86 | name: 'claudeus_plane_projects__delete', 87 | description: 'Deletes an existing project in a workspace', 88 | status: 'enabled', 89 | inputSchema: { 90 | type: 'object', 91 | properties: { 92 | workspace_slug: { 93 | type: 'string', 94 | description: 'The slug of the workspace to delete the project from.' 95 | }, 96 | project_id: { 97 | type: 'string', 98 | description: 'The ID of the project to delete.' 99 | } 100 | } 101 | } 102 | } 103 | ]; ``` -------------------------------------------------------------------------------- /src/prompts/projects/definitions.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { z } from 'zod'; 2 | import { PromptDefinition } from '../../types/prompt.js'; 3 | import { 4 | analyzeWorkspaceHealthHandler, 5 | suggestResourceAllocationHandler, 6 | recommendProjectStructureHandler 7 | } from './handlers.js'; 8 | 9 | const workspaceSlugSchema = z.string().optional().describe('The workspace slug to analyze. If not provided, all workspaces will be analyzed.'); 10 | const includeArchivedSchema = z.boolean().optional().describe('Whether to include archived projects in the analysis.'); 11 | const focusAreaSchema = z.enum(['members', 'cycles', 'modules']).optional().describe('The area to focus resource allocation analysis on.'); 12 | const templateProjectSchema = z.string().optional().describe('The name of a project to use as a template for structure recommendations.'); 13 | 14 | export const analyzeWorkspaceHealth: PromptDefinition = { 15 | name: 'analyze_workspace_health', 16 | description: 'Analyzes the health of all projects in a workspace, examining member count, cycle/module usage, and activity metrics.', 17 | schema: z.object({ 18 | workspace_slug: workspaceSlugSchema, 19 | include_archived: includeArchivedSchema 20 | }), 21 | examples: [ 22 | { 23 | name: 'Analyze all projects', 24 | args: {} 25 | }, 26 | { 27 | name: 'Analyze specific workspace', 28 | args: { 29 | workspace_slug: 'my-workspace' 30 | } 31 | }, 32 | { 33 | name: 'Include archived projects', 34 | args: { 35 | include_archived: true 36 | } 37 | } 38 | ], 39 | handler: analyzeWorkspaceHealthHandler 40 | }; 41 | 42 | export const suggestResourceAllocation: PromptDefinition = { 43 | name: 'suggest_resource_allocation', 44 | description: 'Suggests optimal resource allocation across projects based on member count, project size, and activity.', 45 | schema: z.object({ 46 | workspace_slug: workspaceSlugSchema, 47 | focus_area: focusAreaSchema 48 | }), 49 | examples: [ 50 | { 51 | name: 'Analyze member allocation', 52 | args: { 53 | focus_area: 'members' 54 | } 55 | }, 56 | { 57 | name: 'Analyze cycle usage', 58 | args: { 59 | focus_area: 'cycles' 60 | } 61 | }, 62 | { 63 | name: 'Analyze module usage in workspace', 64 | args: { 65 | workspace_slug: 'my-workspace', 66 | focus_area: 'modules' 67 | } 68 | } 69 | ], 70 | handler: suggestResourceAllocationHandler 71 | }; 72 | 73 | export const recommendProjectStructure: PromptDefinition = { 74 | name: 'recommend_project_structure', 75 | description: 'Analyzes project structures and provides recommendations for standardization and best practices.', 76 | schema: z.object({ 77 | workspace_slug: workspaceSlugSchema, 78 | template_project: templateProjectSchema 79 | }), 80 | examples: [ 81 | { 82 | name: 'Use best practices', 83 | args: {} 84 | }, 85 | { 86 | name: 'Use template project', 87 | args: { 88 | template_project: 'ideal-project' 89 | } 90 | }, 91 | { 92 | name: 'Analyze specific workspace', 93 | args: { 94 | workspace_slug: 'my-workspace' 95 | } 96 | } 97 | ], 98 | handler: recommendProjectStructureHandler 99 | }; 100 | ``` -------------------------------------------------------------------------------- /src/tools/projects/list.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { z } from 'zod'; 2 | import { Tool, ToolResponse } from '../../types/mcp.js'; 3 | import { PlaneApiClient } from '../../api/client.js'; 4 | 5 | const inputSchema = { 6 | type: 'object', 7 | properties: { 8 | workspace_slug: { 9 | type: 'string', 10 | description: 'The workspace to list projects from. If not provided, the default workspace will be used.' 11 | } 12 | } 13 | }; 14 | 15 | const zodInputSchema = z.object({ 16 | workspace_slug: z.string().optional().describe('The workspace to list projects from. If not provided, the default workspace will be used.') 17 | }); 18 | 19 | interface Project { 20 | id: string; 21 | name: string; 22 | identifier: string; 23 | [key: string]: unknown; 24 | } 25 | 26 | interface PaginatedResponse { 27 | results: Project[]; 28 | [key: string]: unknown; 29 | } 30 | 31 | /** 32 | * Tool for listing projects in a Plane workspace. 33 | * If no workspace is specified, the default workspace from the client's configuration will be used. 34 | */ 35 | export class ListProjectsTool implements Tool { 36 | name = 'claudeus_plane_projects__list'; 37 | description = 'Lists all projects in a Plane workspace'; 38 | inputSchema = inputSchema; 39 | 40 | constructor(private client: PlaneApiClient) {} 41 | 42 | async execute(args: Record<string, unknown>): Promise<ToolResponse> { 43 | try { 44 | const input = zodInputSchema.parse(args); 45 | const workspace = input.workspace_slug || this.client.instance.defaultWorkspace; 46 | 47 | this.client.notify({ 48 | type: 'info', 49 | message: 'Fetching projects', 50 | source: this.name, 51 | data: {} 52 | }); 53 | 54 | const response = await this.client.listProjects(workspace); 55 | const projects = 'results' in response ? response.results : response; 56 | 57 | this.client.notify({ 58 | type: 'success', 59 | message: `Successfully retrieved ${projects.length} projects`, 60 | source: this.name, 61 | data: { 62 | workspace, 63 | projectCount: projects.length 64 | } 65 | }); 66 | 67 | return { 68 | isError: false, 69 | content: [{ 70 | type: 'text', 71 | text: `Successfully retrieved ${projects.length} projects: ${JSON.stringify(projects)}` 72 | }] 73 | }; 74 | } catch (error) { 75 | const errorMessage = error instanceof Error ? error.message : 'Unknown error'; 76 | this.client.notify({ 77 | type: 'error', 78 | message: `Failed to list projects: ${errorMessage}`, 79 | source: this.name, 80 | data: { 81 | error: errorMessage 82 | } 83 | }); 84 | 85 | return { 86 | isError: true, 87 | content: [{ 88 | type: 'text', 89 | text: `Failed to list projects: ${errorMessage}` 90 | }] 91 | }; 92 | } 93 | } 94 | } ``` -------------------------------------------------------------------------------- /smithery.yaml: -------------------------------------------------------------------------------- ```yaml 1 | # Smithery.ai configuration for Claudeus Plane MCP 2 | name: "Claudeus Plane MCP" 3 | description: "AI-powered Plane project management with MCP protocol support" 4 | version: "1.0.0" 5 | author: 6 | name: "Amadeus Samiel H." 7 | email: "[email protected]" 8 | organization: 9 | name: "SimHop IT & Media AB" 10 | website: "https://simhop.se" 11 | 12 | startCommand: 13 | type: stdio 14 | configSchema: 15 | type: object 16 | properties: 17 | PLANE_INSTANCES_PATH: 18 | type: string 19 | description: "Path to Plane instances configuration JSON file" 20 | default: "./plane-instances.json" 21 | examples: ["./plane-instances.json", "/app/config/plane-instances.json"] 22 | PORT: 23 | type: number 24 | description: "Port number for the MCP server (for health checks)" 25 | default: 3000 26 | minimum: 1024 27 | maximum: 65535 28 | NODE_ENV: 29 | type: string 30 | description: "Node environment (development/production)" 31 | enum: ["development", "production"] 32 | default: "production" 33 | DEBUG: 34 | type: string 35 | description: "Debug configuration pattern" 36 | default: "claudeus:*" 37 | examples: ["claudeus:*", "claudeus:plane,claudeus:mcp"] 38 | AUTH_TYPE: 39 | type: string 40 | description: "Default Plane authentication type" 41 | enum: ["api_key"] 42 | default: "api_key" 43 | SSL_VERIFY: 44 | type: boolean 45 | description: "Verify SSL certificates for Plane connections" 46 | default: true 47 | LOG_LEVEL: 48 | type: string 49 | description: "Logging level" 50 | enum: ["error", "warn", "info", "debug"] 51 | default: "info" 52 | BATCH_SIZE: 53 | type: number 54 | description: "Maximum number of items to process in a batch" 55 | default: 100 56 | minimum: 1 57 | maximum: 1000 58 | additionalProperties: false 59 | 60 | commandFunction: |- 61 | (config) => { 62 | // Ensure configuration files exist 63 | const fs = require('fs'); 64 | const path = require('path'); 65 | 66 | // Helper function to copy example if target doesn't exist 67 | const copyExampleIfNeeded = (examplePath, targetPath) => { 68 | if (!fs.existsSync(targetPath) && fs.existsSync(examplePath)) { 69 | fs.copyFileSync(examplePath, targetPath); 70 | } 71 | }; 72 | 73 | // Copy example files if needed 74 | copyExampleIfNeeded('plane-instances.json.example', 'plane-instances.json'); 75 | copyExampleIfNeeded('.env.example', '.env'); 76 | 77 | const env = { 78 | PLANE_INSTANCES_PATH: config.PLANE_INSTANCES_PATH || "./plane-instances.json", 79 | PORT: config.PORT?.toString() || "3000", 80 | NODE_ENV: config.NODE_ENV || "production", 81 | DEBUG: config.DEBUG || "claudeus:*", 82 | AUTH_TYPE: config.AUTH_TYPE || "api_key", 83 | SSL_VERIFY: (config.SSL_VERIFY ?? true).toString(), 84 | LOG_LEVEL: config.LOG_LEVEL || "info", 85 | BATCH_SIZE: config.BATCH_SIZE?.toString() || "100", 86 | MCP_STDIO: "true" 87 | }; 88 | 89 | return { 90 | command: "node", 91 | args: ["dist/index.js"], 92 | env, 93 | cwd: process.cwd() 94 | }; 95 | } 96 | 97 | capabilities: 98 | prompts: 99 | listChanged: true 100 | tools: 101 | listChanged: true 102 | resources: 103 | listChanged: true 104 | 105 | security: 106 | userConsent: 107 | required: true 108 | description: "This MCP server requires access to your Plane instances and will perform project management operations." 109 | dataAccess: 110 | - type: "plane" 111 | description: "Access to configured Plane instances via REST API" 112 | - type: "filesystem" 113 | description: "Access to plane-instances.json configuration file" 114 | toolSafety: 115 | confirmationRequired: true 116 | description: "Tools can modify Plane projects, issues, and settings" 117 | dangerousOperations: 118 | - "delete_project" 119 | - "delete_issue" 120 | - "delete_cycle" 121 | - "delete_module" 122 | - "delete_workspace" ``` -------------------------------------------------------------------------------- /src/tools/issues/list.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { Tool, ToolResponse } from '../../types/mcp.js'; 2 | import { IssuesClient } from '../../api/issues/client.js'; 3 | import { IssueListFilters, IssueListResponse, IssuePriority } from '../../api/issues/types.js'; 4 | import { PlaneInstanceConfig } from '../../api/types/config.js'; 5 | 6 | export class ListIssuesTools implements Tool { 7 | private issuesClient: IssuesClient; 8 | private instance: PlaneInstanceConfig; 9 | 10 | name = 'claudeus_plane_issues__list'; 11 | description = 'Lists issues in a Plane project'; 12 | status = 'enabled' as const; 13 | inputSchema = { 14 | type: 'object', 15 | properties: { 16 | workspace_slug: { 17 | type: 'string', 18 | description: 'The slug of the workspace to list issues from. If not provided, uses the default workspace.' 19 | }, 20 | project_id: { 21 | type: 'string', 22 | description: 'The ID of the project to list issues from' 23 | }, 24 | state: { 25 | type: 'string', 26 | description: 'Filter issues by state ID' 27 | }, 28 | priority: { 29 | type: 'string', 30 | enum: ['urgent', 'high', 'medium', 'low', 'none'], 31 | description: 'Filter issues by priority' 32 | }, 33 | assignee: { 34 | type: 'string', 35 | description: 'Filter issues by assignee ID' 36 | }, 37 | label: { 38 | type: 'string', 39 | description: 'Filter issues by label ID' 40 | }, 41 | created_by: { 42 | type: 'string', 43 | description: 'Filter issues by creator ID' 44 | }, 45 | start_date: { 46 | type: 'string', 47 | format: 'date', 48 | description: 'Filter issues by start date (YYYY-MM-DD)' 49 | }, 50 | target_date: { 51 | type: 'string', 52 | format: 'date', 53 | description: 'Filter issues by target date (YYYY-MM-DD)' 54 | }, 55 | subscriber: { 56 | type: 'string', 57 | description: 'Filter issues by subscriber ID' 58 | }, 59 | is_draft: { 60 | type: 'boolean', 61 | description: 'Filter draft issues', 62 | default: false 63 | }, 64 | archived: { 65 | type: 'boolean', 66 | description: 'Filter archived issues', 67 | default: false 68 | }, 69 | page: { 70 | type: 'number', 71 | description: 'Page number (1-based)', 72 | default: 1 73 | }, 74 | page_size: { 75 | type: 'number', 76 | description: 'Number of items per page', 77 | default: 100 78 | } 79 | }, 80 | required: ['project_id'] 81 | }; 82 | 83 | constructor(instance: PlaneInstanceConfig) { 84 | this.instance = instance; 85 | this.issuesClient = new IssuesClient(this.instance); 86 | } 87 | 88 | async execute(args: Record<string, unknown>): Promise<ToolResponse> { 89 | const input = args as { 90 | workspace_slug?: string; 91 | project_id: string; 92 | state?: string; 93 | priority?: IssuePriority; 94 | assignee?: string; 95 | label?: string; 96 | created_by?: string; 97 | start_date?: string; 98 | target_date?: string; 99 | subscriber?: string; 100 | is_draft?: boolean; 101 | archived?: boolean; 102 | page?: number; 103 | page_size?: number; 104 | }; 105 | 106 | const { 107 | workspace_slug = this.instance.defaultWorkspace, 108 | project_id, 109 | page = 1, 110 | page_size = 100, 111 | ...filters 112 | } = input; 113 | 114 | // Validate workspace 115 | if (!workspace_slug) { 116 | return { 117 | isError: true, 118 | content: [{ 119 | type: 'text', 120 | text: 'Workspace slug is required' 121 | }] 122 | }; 123 | } 124 | 125 | // Validate project ID 126 | if (!project_id) { 127 | return { 128 | isError: true, 129 | content: [{ 130 | type: 'text', 131 | text: 'Project ID is required' 132 | }] 133 | }; 134 | } 135 | 136 | try { 137 | const response = await this.issuesClient.list( 138 | workspace_slug, 139 | project_id, 140 | filters as IssueListFilters, 141 | page, 142 | page_size 143 | ); 144 | 145 | return { 146 | content: [{ 147 | type: 'text', 148 | text: JSON.stringify(response) 149 | }] 150 | }; 151 | } catch (error: unknown) { 152 | const errorMessage = error instanceof Error ? error.message : 'Unknown error'; 153 | return { 154 | isError: true, 155 | content: [{ 156 | type: 'text', 157 | text: `Failed to list issues: ${errorMessage}` 158 | }] 159 | }; 160 | } 161 | } 162 | } 163 | ``` -------------------------------------------------------------------------------- /src/api/base-client.ts: -------------------------------------------------------------------------------- ```typescript 1 | import axios, { AxiosInstance, AxiosError } from 'axios'; 2 | import { PlaneInstanceConfig } from './types/config.js'; 3 | 4 | export type QueryParams = Record<string, string | number | boolean | Array<string | number> | null | undefined>; 5 | 6 | interface PlaneErrorResponse { 7 | message: string; 8 | [key: string]: any; 9 | } 10 | 11 | export class BaseApiClient { 12 | protected client: AxiosInstance; 13 | protected _instance: PlaneInstanceConfig; 14 | public readonly baseUrl: string; 15 | 16 | constructor(instance: PlaneInstanceConfig) { 17 | this._instance = instance; 18 | this.baseUrl = instance.baseUrl; 19 | 20 | // Keep the full baseUrl including /api/v1 since it's part of the base URL 21 | const baseURL = this.baseUrl.endsWith('/') 22 | ? this.baseUrl.slice(0, -1) // Remove trailing slash if present 23 | : this.baseUrl; 24 | 25 | this.client = axios.create({ 26 | baseURL, 27 | headers: { 28 | 'X-API-Key': instance.apiKey, 29 | 'Content-Type': 'application/json', 30 | 'Accept': 'application/json' 31 | } 32 | }); 33 | 34 | // Add response interceptor for better error handling 35 | this.client.interceptors.response.use( 36 | response => response, 37 | error => { 38 | if (axios.isAxiosError(error)) { 39 | const axiosError = error as AxiosError<PlaneErrorResponse>; 40 | if (axiosError.response?.status === 403) { 41 | throw new Error(`API Error (403): Authentication failed. Please check your API key.`); 42 | } 43 | const errorMessage = axiosError.response?.data?.message || axiosError.message; 44 | const errorCode = axiosError.response?.status; 45 | throw new Error(`API Error (${errorCode}): ${errorMessage}`); 46 | } 47 | throw error; 48 | } 49 | ); 50 | } 51 | 52 | get instance(): PlaneInstanceConfig { 53 | return this._instance; 54 | } 55 | 56 | protected handleError(error: AxiosError<PlaneErrorResponse>): never { 57 | if (error.response?.status === 403) { 58 | throw new Error(`API Error: Authentication failed. Please check your API key.`); 59 | } 60 | if (error.response?.data?.message) { 61 | throw new Error(`API Error: ${error.response.data.message}`); 62 | } else if (error.response?.status) { 63 | throw new Error(`HTTP Error ${error.response.status}: ${error.message}`); 64 | } else { 65 | throw new Error(`Network Error: ${error.message}`); 66 | } 67 | } 68 | 69 | protected async get<T>(endpoint: string, params?: QueryParams): Promise<T> { 70 | try { 71 | const response = await this.client.get<T>(endpoint, { params }); 72 | return response.data; 73 | } catch (error) { 74 | this.handleError(error as AxiosError<PlaneErrorResponse>); 75 | } 76 | } 77 | 78 | protected async post<T, D = Record<string, unknown>>(endpoint: string, data: D): Promise<T> { 79 | try { 80 | const response = await this.client.post<T>(endpoint, data); 81 | return response.data; 82 | } catch (error) { 83 | this.handleError(error as AxiosError<PlaneErrorResponse>); 84 | } 85 | } 86 | 87 | protected async put<T, D = Record<string, unknown>>(endpoint: string, data: D): Promise<T> { 88 | try { 89 | const response = await this.client.put<T>(endpoint, data); 90 | return response.data; 91 | } catch (error) { 92 | this.handleError(error as AxiosError<PlaneErrorResponse>); 93 | } 94 | } 95 | 96 | protected async delete<T>(endpoint: string): Promise<T> { 97 | try { 98 | const response = await this.client.delete<T>(endpoint); 99 | return response.data; 100 | } catch (error) { 101 | this.handleError(error as AxiosError<PlaneErrorResponse>); 102 | } 103 | } 104 | 105 | protected async patch<T, D = Record<string, unknown>>(endpoint: string, data: D): Promise<T> { 106 | try { 107 | const response = await this.client.patch<T>(endpoint, data); 108 | return response.data; 109 | } catch (error) { 110 | this.handleError(error as AxiosError<PlaneErrorResponse>); 111 | } 112 | } 113 | } ``` -------------------------------------------------------------------------------- /src/tools/projects/__tests__/delete.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { describe, it, expect, beforeEach, vi } from 'vitest'; 2 | import { DeleteProjectTool } from '../delete.js'; 3 | import { PlaneApiClient } from '../../../api/client.js'; 4 | import { PlaneInstance } from '../../../config/plane-config.js'; 5 | 6 | // Mock the PlaneApiClient 7 | vi.mock('../../../api/client.js', () => { 8 | const mockNotify = vi.fn(); 9 | return { 10 | PlaneApiClient: vi.fn().mockImplementation((instance, context) => ({ 11 | instance, 12 | deleteProject: vi.fn(), 13 | notify: mockNotify 14 | })) 15 | }; 16 | }); 17 | 18 | describe('DeleteProjectTool', () => { 19 | let tool: DeleteProjectTool; 20 | let mockClient: PlaneApiClient; 21 | 22 | beforeEach(() => { 23 | // Create a mock instance 24 | const mockInstance: PlaneInstance = { 25 | name: 'test', 26 | baseUrl: 'https://test.plane.so', 27 | apiKey: 'test-key', 28 | defaultWorkspace: 'test-workspace' 29 | }; 30 | 31 | // Create a mock context 32 | const mockContext = { 33 | progressToken: '123', 34 | workspace: 'test-workspace' 35 | }; 36 | 37 | // Create a mock client 38 | mockClient = new PlaneApiClient(mockInstance, mockContext); 39 | 40 | // Create the tool instance 41 | tool = new DeleteProjectTool(mockClient); 42 | 43 | // Reset mock call history 44 | vi.clearAllMocks(); 45 | }); 46 | 47 | it('should delete a project successfully', async () => { 48 | (mockClient.deleteProject as any).mockResolvedValue(undefined); 49 | 50 | const result = await tool.execute({ 51 | project_id: 'test-id' 52 | }); 53 | 54 | expect(mockClient.deleteProject).toHaveBeenCalledWith('test-workspace', 'test-id'); 55 | expect(JSON.parse(result.content[0].text)).toEqual({ 56 | success: true, 57 | message: 'Project deleted successfully', 58 | project_id: 'test-id', 59 | workspace: 'test-workspace' 60 | }); 61 | }); 62 | 63 | it('should use provided workspace instead of default', async () => { 64 | (mockClient.deleteProject as any).mockResolvedValue(undefined); 65 | 66 | await tool.execute({ 67 | workspace_slug: 'custom-workspace', 68 | project_id: 'test-id' 69 | }); 70 | 71 | expect(mockClient.deleteProject).toHaveBeenCalledWith('custom-workspace', 'test-id'); 72 | }); 73 | 74 | it('should handle API errors', async () => { 75 | const errorMessage = 'API Error: Project deletion failed'; 76 | (mockClient.deleteProject as any).mockRejectedValue(new Error(errorMessage)); 77 | 78 | const result = await tool.execute({ 79 | project_id: 'test-id' 80 | }); 81 | 82 | expect(result.isError).toBe(true); 83 | expect(result.content[0].text).toBe(`Error: ${errorMessage}`); 84 | expect(mockClient.notify).toHaveBeenCalledWith({ 85 | type: 'error', 86 | message: `Failed to delete project: ${errorMessage}`, 87 | source: 'claudeus_plane_projects__delete', 88 | data: { 89 | error: errorMessage, 90 | workspace: 'test-workspace', 91 | project_id: 'test-id' 92 | } 93 | }); 94 | }); 95 | 96 | it('should validate required fields', async () => { 97 | await expect(tool.execute({ 98 | // Missing project_id 99 | })).rejects.toThrow(); 100 | }); 101 | 102 | it('should handle missing workspace configuration', async () => { 103 | const mockInstanceNoWorkspace: PlaneInstance = { 104 | name: 'test', 105 | baseUrl: 'https://test.plane.so', 106 | apiKey: 'test-key' 107 | // No defaultWorkspace 108 | }; 109 | 110 | const mockContextNoWorkspace = { 111 | progressToken: '123', 112 | workspace: 'test-workspace' 113 | }; 114 | 115 | const clientNoWorkspace = new PlaneApiClient(mockInstanceNoWorkspace, mockContextNoWorkspace); 116 | (clientNoWorkspace.deleteProject as any).mockResolvedValue(undefined); 117 | 118 | const toolNoWorkspace = new DeleteProjectTool(clientNoWorkspace); 119 | 120 | const result = await toolNoWorkspace.execute({ 121 | project_id: 'test-id' 122 | // No workspace_slug provided 123 | }); 124 | 125 | expect(result.isError).toBe(true); 126 | expect(result.content[0].text).toBe('Error: No workspace provided or configured'); 127 | }); 128 | }); ``` -------------------------------------------------------------------------------- /src/tools/issues/create.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { Tool, ToolResponse } from '../../types/mcp.js'; 2 | import { IssuesClient } from '../../api/issues/client.js'; 3 | import { CreateIssueData, IssuePriority } from '../../api/issues/types.js'; 4 | import { PlaneInstanceConfig } from '../../api/types/config.js'; 5 | 6 | interface CreateIssueInput extends CreateIssueData { 7 | workspace_slug?: string; 8 | project_id: string; 9 | } 10 | 11 | export class CreateIssueTool implements Tool { 12 | private issuesClient: IssuesClient; 13 | private instance: PlaneInstanceConfig; 14 | 15 | name = 'claudeus_plane_issues__create'; 16 | description = 'Creates a new issue in a Plane project'; 17 | status = 'enabled' as const; 18 | inputSchema = { 19 | type: 'object', 20 | properties: { 21 | workspace_slug: { 22 | type: 'string', 23 | description: 'The slug of the workspace to create the issue in. If not provided, uses the default workspace.' 24 | }, 25 | project_id: { 26 | type: 'string', 27 | description: 'The ID of the project to create the issue in' 28 | }, 29 | name: { 30 | type: 'string', 31 | description: 'The name/title of the issue' 32 | }, 33 | description_html: { 34 | type: 'string', 35 | description: 'The HTML description of the issue' 36 | }, 37 | priority: { 38 | type: 'string', 39 | enum: ['urgent', 'high', 'medium', 'low', 'none'], 40 | description: 'The priority of the issue', 41 | default: 'none' 42 | }, 43 | start_date: { 44 | type: 'string', 45 | format: 'date', 46 | description: 'The start date of the issue (YYYY-MM-DD)' 47 | }, 48 | target_date: { 49 | type: 'string', 50 | format: 'date', 51 | description: 'The target date of the issue (YYYY-MM-DD)' 52 | }, 53 | estimate_point: { 54 | type: 'number', 55 | description: 'Story points or time estimate for the issue' 56 | }, 57 | state: { 58 | type: 'string', 59 | description: 'The state ID for the issue' 60 | }, 61 | assignees: { 62 | type: 'array', 63 | items: { 64 | type: 'string' 65 | }, 66 | description: 'Array of user IDs to assign to the issue' 67 | }, 68 | labels: { 69 | type: 'array', 70 | items: { 71 | type: 'string' 72 | }, 73 | description: 'Array of label IDs to apply to the issue' 74 | }, 75 | parent: { 76 | type: 'string', 77 | description: 'ID of the parent issue (for sub-issues)' 78 | }, 79 | is_draft: { 80 | type: 'boolean', 81 | description: 'Whether this is a draft issue', 82 | default: false 83 | } 84 | }, 85 | required: ['project_id', 'name'] 86 | }; 87 | 88 | constructor(instance: PlaneInstanceConfig) { 89 | this.instance = instance; 90 | this.issuesClient = new IssuesClient(this.instance); 91 | } 92 | 93 | async execute(args: Record<string, unknown>): Promise<ToolResponse> { 94 | // Type cast with validation 95 | const input = args as unknown as CreateIssueInput; 96 | if (!this.validateInput(input)) { 97 | return { 98 | isError: true, 99 | content: [{ 100 | type: 'text', 101 | text: 'Invalid input: missing required fields' 102 | }] 103 | }; 104 | } 105 | 106 | const { 107 | workspace_slug = this.instance.defaultWorkspace, 108 | project_id, 109 | ...issueData 110 | } = input; 111 | 112 | // Validate workspace 113 | if (!workspace_slug) { 114 | return { 115 | isError: true, 116 | content: [{ 117 | type: 'text', 118 | text: 'Workspace slug is required' 119 | }] 120 | }; 121 | } 122 | 123 | // Validate project ID 124 | if (!project_id) { 125 | return { 126 | isError: true, 127 | content: [{ 128 | type: 'text', 129 | text: 'Project ID is required' 130 | }] 131 | }; 132 | } 133 | 134 | // Validate name 135 | if (!issueData.name) { 136 | return { 137 | isError: true, 138 | content: [{ 139 | type: 'text', 140 | text: 'Issue name is required' 141 | }] 142 | }; 143 | } 144 | 145 | try { 146 | const response = await this.issuesClient.create( 147 | workspace_slug, 148 | project_id, 149 | issueData 150 | ); 151 | 152 | return { 153 | content: [{ 154 | type: 'text', 155 | text: JSON.stringify(response) 156 | }] 157 | }; 158 | } catch (error: unknown) { 159 | const errorMessage = error instanceof Error ? error.message : 'Unknown error'; 160 | return { 161 | isError: true, 162 | content: [{ 163 | type: 'text', 164 | text: `Failed to create issue: ${errorMessage}` 165 | }] 166 | }; 167 | } 168 | } 169 | 170 | private validateInput(input: unknown): input is CreateIssueInput { 171 | if (typeof input !== 'object' || input === null) return false; 172 | const data = input as Record<string, unknown>; 173 | return typeof data.name === 'string' && typeof data.project_id === 'string'; 174 | } 175 | } 176 | ``` -------------------------------------------------------------------------------- /src/tools/projects/handlers.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { ToolResponse } from '../../types/mcp.js'; 2 | import { ProjectsAPI } from '../../api/projects.js'; 3 | import { z } from 'zod'; 4 | import { CreateProjectSchema, UpdateProjectSchema } from '../../api/types/project.js'; 5 | 6 | const listProjectsSchema = z.object({ 7 | workspace_slug: z.string().optional(), 8 | include_archived: z.boolean().optional() 9 | }); 10 | 11 | export async function listProjects( 12 | api: ProjectsAPI, 13 | args: Record<string, unknown> 14 | ): Promise<ToolResponse> { 15 | const { workspace_slug, include_archived } = listProjectsSchema.parse(args); 16 | 17 | try { 18 | const workspace = workspace_slug || api.instance.defaultWorkspace; 19 | if (!workspace) { 20 | throw new Error('No workspace provided or configured'); 21 | } 22 | 23 | const projects = await api.listProjects(workspace, { include_archived }); 24 | 25 | return { 26 | content: [{ 27 | type: 'text', 28 | text: JSON.stringify(projects, null, 2) 29 | }] 30 | }; 31 | } catch (error) { 32 | if (error instanceof Error) { 33 | throw new Error(`Failed to list projects: ${error.message}`); 34 | } 35 | throw error; 36 | } 37 | } 38 | 39 | const createProjectSchema = z.object({ 40 | workspace_slug: z.string().optional(), 41 | name: z.string(), 42 | identifier: z.string(), 43 | description: z.string().optional(), 44 | project_lead: z.string().uuid().optional(), 45 | default_assignee: z.string().uuid().optional() 46 | }); 47 | 48 | export async function createProject( 49 | api: ProjectsAPI, 50 | args: Record<string, unknown> 51 | ): Promise<ToolResponse> { 52 | const data = createProjectSchema.parse(args); 53 | const workspace = data.workspace_slug || api.instance.defaultWorkspace; 54 | 55 | if (!workspace) { 56 | throw new Error('No workspace provided or configured'); 57 | } 58 | 59 | try { 60 | const project = await api.createProject(workspace, data); 61 | return { 62 | content: [{ 63 | type: 'text', 64 | text: JSON.stringify(project, null, 2) 65 | }] 66 | }; 67 | } catch (error) { 68 | if (error instanceof Error) { 69 | throw new Error(`Failed to create project: ${error.message}`); 70 | } 71 | throw error; 72 | } 73 | } 74 | 75 | const updateProjectSchema = z.object({ 76 | workspace_slug: z.string().optional(), 77 | project_id: z.string(), 78 | name: z.string().optional(), 79 | description: z.string().optional(), 80 | project_lead: z.string().uuid().optional(), 81 | default_assignee: z.string().uuid().optional() 82 | }); 83 | 84 | export async function updateProject( 85 | api: ProjectsAPI, 86 | args: Record<string, unknown> 87 | ): Promise<ToolResponse> { 88 | const { workspace_slug, project_id, ...updateData } = updateProjectSchema.parse(args); 89 | const workspace = workspace_slug || api.instance.defaultWorkspace; 90 | 91 | if (!workspace) { 92 | throw new Error('No workspace provided or configured'); 93 | } 94 | 95 | try { 96 | const project = await api.updateProject(workspace, project_id, updateData); 97 | return { 98 | content: [{ 99 | type: 'text', 100 | text: JSON.stringify(project, null, 2) 101 | }] 102 | }; 103 | } catch (error) { 104 | if (error instanceof Error) { 105 | throw new Error(`Failed to update project: ${error.message}`); 106 | } 107 | throw error; 108 | } 109 | } 110 | 111 | const deleteProjectSchema = z.object({ 112 | workspace_slug: z.string().optional(), 113 | project_id: z.string() 114 | }); 115 | 116 | export async function deleteProject( 117 | api: ProjectsAPI, 118 | args: Record<string, unknown> 119 | ): Promise<ToolResponse> { 120 | const { workspace_slug, project_id } = deleteProjectSchema.parse(args); 121 | const workspace = workspace_slug || api.instance.defaultWorkspace; 122 | 123 | if (!workspace) { 124 | throw new Error('No workspace provided or configured'); 125 | } 126 | 127 | try { 128 | await api.deleteProject(workspace, project_id); 129 | return { 130 | content: [{ 131 | type: 'text', 132 | text: JSON.stringify({ success: true, message: 'Project deleted successfully' }) 133 | }] 134 | }; 135 | } catch (error) { 136 | if (error instanceof Error) { 137 | throw new Error(`Failed to delete project: ${error.message}`); 138 | } 139 | throw error; 140 | } 141 | } 142 | 143 | export async function handleProjectTools( 144 | api: ProjectsAPI, 145 | name: string, 146 | args: Record<string, unknown> 147 | ): Promise<ToolResponse> { 148 | switch (name) { 149 | case 'claudeus_plane_projects__list': 150 | return listProjects(api, args); 151 | case 'claudeus_plane_projects__create': 152 | return createProject(api, args); 153 | case 'claudeus_plane_projects__update': 154 | return updateProject(api, args); 155 | case 'claudeus_plane_projects__delete': 156 | return deleteProject(api, args); 157 | default: 158 | throw new Error(`Unknown project tool: ${name}`); 159 | } 160 | } ``` -------------------------------------------------------------------------------- /src/test/integration/projects.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { describe, it, expect, beforeAll, afterAll } from 'vitest'; 2 | import { PlaneApiClient } from '../../api/client.js'; 3 | import { loadPlaneConfig } from '../../config/plane-config.js'; 4 | import { PlaneInstanceConfig } from '../../api/types/config.js'; 5 | import { PromptContext } from '../../types/prompt.js'; 6 | import { CreateProjectTool } from '../../tools/projects/create.js'; 7 | import { UpdateProjectTool } from '../../tools/projects/update.js'; 8 | import { DeleteProjectTool } from '../../tools/projects/delete.js'; 9 | import { ListProjectsTool } from '../../tools/projects/list.js'; 10 | 11 | const TEST_CONFIG = { 12 | instanceName: 'simhop_test', 13 | projectPrefix: 'TEST_PROJ_', 14 | timeouts: { 15 | create: 5000, 16 | query: 3000, 17 | update: 5000, 18 | delete: 5000 19 | } 20 | }; 21 | 22 | describe('Project Management Integration', () => { 23 | let client: PlaneApiClient; 24 | let createTool: CreateProjectTool; 25 | let listTool: ListProjectsTool; 26 | let updateTool: UpdateProjectTool; 27 | let deleteTool: DeleteProjectTool; 28 | let testProjectId: string; 29 | 30 | // Test project data 31 | const projectIdentifier = `${TEST_CONFIG.projectPrefix}${Date.now()}`; 32 | const initialProjectData = { 33 | name: 'Integration Test Project', 34 | identifier: projectIdentifier, 35 | description: 'Project created by integration tests', 36 | network: 0, // Private 37 | emoji: '1f9ea', // Test tube emoji 38 | module_view: true, 39 | cycle_view: true, 40 | issue_views_view: true, 41 | page_view: true, 42 | inbox_view: true 43 | }; 44 | 45 | beforeAll(async () => { 46 | // Set test config path for loading 47 | process.env.PLANE_INSTANCES_PATH = process.env.TEST_PLANE_INSTANCE_PATH || './plane-instances-test.json'; 48 | 49 | // Load test configuration 50 | const instances = await loadPlaneConfig(); 51 | const instance = instances[TEST_CONFIG.instanceName]; 52 | if (!instance) { 53 | throw new Error(`Test instance "${TEST_CONFIG.instanceName}" not found in configuration`); 54 | } 55 | 56 | // Create test context 57 | const context: PromptContext = { 58 | progressToken: 'test', 59 | workspace: instance.defaultWorkspace 60 | }; 61 | 62 | // Initialize client and tools 63 | client = new PlaneApiClient(instance, context); 64 | createTool = new CreateProjectTool(client); 65 | listTool = new ListProjectsTool(client); 66 | updateTool = new UpdateProjectTool(client); 67 | deleteTool = new DeleteProjectTool(client); 68 | }); 69 | 70 | afterAll(async () => { 71 | // Cleanup: Delete test project if it exists 72 | if (testProjectId) { 73 | try { 74 | await deleteTool.execute({ project_id: testProjectId }); 75 | } catch (error) { 76 | console.warn('Failed to cleanup test project:', error); 77 | } 78 | } 79 | }); 80 | 81 | it('should create a new test project', async () => { 82 | const result = await createTool.execute(initialProjectData); 83 | expect(result.isError).toBe(false); 84 | 85 | const responseText = result.content[0].text; 86 | expect(responseText).toContain('Successfully created project'); 87 | 88 | // Extract project ID from response text 89 | const match = responseText.match(/ID: ([^)]+)/); 90 | expect(match).toBeTruthy(); 91 | testProjectId = match![1]; 92 | expect(testProjectId).toBeTruthy(); 93 | }, TEST_CONFIG.timeouts.create); 94 | 95 | it('should list projects and find the new project', async () => { 96 | const result = await listTool.execute({}); 97 | expect(result.isError).toBe(false); 98 | 99 | const responseText = result.content[0].text; 100 | expect(responseText).toContain('Successfully retrieved'); 101 | 102 | // Extract projects from response 103 | const match = responseText.match(/\[(.*)\]/); 104 | expect(match).toBeTruthy(); 105 | const projects = JSON.parse(match![1]); 106 | 107 | const testProject = projects.find((p: any) => p.id === testProjectId); 108 | expect(testProject).toBeTruthy(); 109 | expect(testProject.name).toBe(initialProjectData.name); 110 | expect(testProject.identifier).toBe(initialProjectData.identifier); 111 | }, TEST_CONFIG.timeouts.query); 112 | 113 | it('should update the test project', async () => { 114 | const updateData = { 115 | project_id: testProjectId, 116 | name: 'Updated Test Project', 117 | description: 'Updated test project description' 118 | }; 119 | 120 | const result = await updateTool.execute(updateData); 121 | expect(result.isError).toBe(false); 122 | expect(result.content[0].text).toContain('Successfully updated project'); 123 | }, TEST_CONFIG.timeouts.update); 124 | 125 | it('should delete the test project', async () => { 126 | const result = await deleteTool.execute({ project_id: testProjectId }); 127 | expect(result.isError).toBe(false); 128 | expect(result.content[0].text).toContain('Successfully deleted project'); 129 | }, TEST_CONFIG.timeouts.delete); 130 | }); 131 | ``` -------------------------------------------------------------------------------- /src/api/client.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { BaseApiClient } from './base-client.js'; 2 | import { PlaneInstanceConfig } from './types/config.js'; 3 | import { PromptContext } from '../types/prompt.js'; 4 | 5 | export interface NotificationOptions { 6 | type: 'info' | 'error' | 'warning' | 'success'; 7 | message: string; 8 | source: string; 9 | data?: Record<string, unknown>; 10 | } 11 | 12 | export interface ToolExecutionOptions { 13 | progressToken: string; 14 | workspace?: string; 15 | } 16 | 17 | export interface CreateProjectData { 18 | name: string; 19 | identifier: string; 20 | description?: string; 21 | network?: number; 22 | emoji?: string; 23 | icon_prop?: Record<string, unknown>; 24 | module_view?: boolean; 25 | cycle_view?: boolean; 26 | issue_views_view?: boolean; 27 | page_view?: boolean; 28 | inbox_view?: boolean; 29 | cover_image?: string | null; 30 | archive_in?: number; 31 | close_in?: number; 32 | default_assignee?: string | null; 33 | project_lead?: string | null; 34 | estimate?: string | null; 35 | default_state?: string | null; 36 | [key: string]: unknown | undefined; 37 | } 38 | 39 | export interface UpdateProjectData { 40 | name?: string; 41 | description?: string; 42 | network?: number; 43 | emoji?: string; 44 | icon_prop?: Record<string, unknown>; 45 | module_view?: boolean; 46 | cycle_view?: boolean; 47 | issue_views_view?: boolean; 48 | page_view?: boolean; 49 | inbox_view?: boolean; 50 | cover_image?: string | null; 51 | archive_in?: number; 52 | close_in?: number; 53 | default_assignee?: string | null; 54 | project_lead?: string | null; 55 | estimate?: string | null; 56 | default_state?: string | null; 57 | [key: string]: unknown | undefined; 58 | } 59 | 60 | interface Project { 61 | id: string; 62 | name: string; 63 | identifier: string; 64 | description?: string; 65 | network?: number; 66 | emoji?: string; 67 | icon_prop?: Record<string, unknown>; 68 | module_view?: boolean; 69 | cycle_view?: boolean; 70 | issue_views_view?: boolean; 71 | page_view?: boolean; 72 | inbox_view?: boolean; 73 | cover_image?: string | null; 74 | archive_in?: number; 75 | close_in?: number; 76 | default_assignee?: string | null; 77 | project_lead?: string | null; 78 | estimate?: string | null; 79 | default_state?: string | null; 80 | [key: string]: unknown | undefined; 81 | } 82 | 83 | interface PaginatedResponse { 84 | results: Project[]; 85 | count: number; 86 | next: string | null; 87 | previous: string | null; 88 | [key: string]: unknown; 89 | } 90 | 91 | export class PlaneApiClient extends BaseApiClient { 92 | protected _instance: PlaneInstanceConfig; 93 | 94 | constructor(instance: PlaneInstanceConfig, private context: PromptContext) { 95 | super(instance); 96 | this._instance = instance; 97 | } 98 | 99 | get instance(): PlaneInstanceConfig { 100 | return this._instance; 101 | } 102 | 103 | notify(options: NotificationOptions) { 104 | // Format notification as a JSON-RPC notification message 105 | const notification = { 106 | jsonrpc: '2.0', 107 | method: 'notification', 108 | params: { 109 | type: options.type, 110 | message: options.message, 111 | source: options.source, 112 | data: options.data || {} 113 | } 114 | }; 115 | 116 | // Send the notification as a JSON string 117 | process.stdout.write(JSON.stringify(notification) + '\n'); 118 | } 119 | 120 | async listProjects(workspace: string): Promise<Project[] | PaginatedResponse> { 121 | return this.get(`/workspaces/${workspace}/projects`); 122 | } 123 | 124 | async createProject(workspaceSlug: string, data: CreateProjectData): Promise<Project> { 125 | const response = await this.post<Project | PaginatedResponse>(`/workspaces/${workspaceSlug}/projects`, data); 126 | 127 | if (!response) { 128 | throw new Error('Failed to create project: No response from server'); 129 | } 130 | 131 | // Handle paginated response 132 | if ('results' in response && Array.isArray(response.results)) { 133 | // Find the newly created project in the results 134 | const project = response.results.find(p => p.name === data.name && p.identifier === data.identifier); 135 | if (project) { 136 | return project; 137 | } 138 | } 139 | 140 | // Handle direct project response 141 | if ('id' in response) { 142 | return response as Project; 143 | } 144 | 145 | throw new Error('Failed to create project: Invalid response format from server'); 146 | } 147 | 148 | async updateProject(workspaceSlug: string, projectId: string, data: UpdateProjectData): Promise<Project> { 149 | return this.put(`/workspaces/${workspaceSlug}/projects/${projectId}`, data); 150 | } 151 | 152 | async deleteProject(workspaceSlug: string, projectId: string): Promise<void> { 153 | await this.delete(`/workspaces/${workspaceSlug}/projects/${projectId}`); 154 | } 155 | 156 | async executeTool(toolName: string, options: ToolExecutionOptions): Promise<{ content: Array<{ text: string }> }> { 157 | // This is a mock implementation - the actual implementation will be provided by the MCP server 158 | return { 159 | content: [{ 160 | text: '[]' // Default empty array as JSON string 161 | }] 162 | }; 163 | } 164 | } ``` -------------------------------------------------------------------------------- /src/tools/projects/create.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { z } from 'zod'; 2 | import { Tool, ToolResponse } from '../../types/mcp.js'; 3 | import { PlaneApiClient } from '../../api/client.js'; 4 | 5 | const zodInputSchema = z.object({ 6 | workspace_slug: z.string().optional(), 7 | name: z.string(), 8 | identifier: z.string(), 9 | description: z.string().optional(), 10 | network: z.number().min(0).max(2).optional(), 11 | emoji: z.string().optional(), 12 | module_view: z.boolean().optional(), 13 | cycle_view: z.boolean().optional(), 14 | issue_views_view: z.boolean().optional(), 15 | page_view: z.boolean().optional(), 16 | inbox_view: z.boolean().optional() 17 | }); 18 | 19 | export class CreateProjectTool implements Tool { 20 | name = 'claudeus_plane_projects__create'; 21 | description = 'Creates a new project in a Plane workspace'; 22 | inputSchema = { 23 | type: 'object', 24 | properties: { 25 | workspace_slug: { 26 | type: 'string', 27 | description: 'The workspace to create the project in. If not provided, the default workspace will be used.' 28 | }, 29 | name: { 30 | type: 'string', 31 | description: 'The name of the project' 32 | }, 33 | identifier: { 34 | type: 'string', 35 | description: 'The unique identifier for the project' 36 | }, 37 | description: { 38 | type: 'string', 39 | description: 'A description of the project' 40 | }, 41 | network: { 42 | type: 'number', 43 | description: 'The network visibility of the project (0: Private, 1: Public, 2: Internal)' 44 | }, 45 | emoji: { 46 | type: 'string', 47 | description: 'The emoji to use for the project' 48 | }, 49 | module_view: { 50 | type: 'boolean', 51 | description: 'Whether to enable module view' 52 | }, 53 | cycle_view: { 54 | type: 'boolean', 55 | description: 'Whether to enable cycle view' 56 | }, 57 | issue_views_view: { 58 | type: 'boolean', 59 | description: 'Whether to enable issue views' 60 | }, 61 | page_view: { 62 | type: 'boolean', 63 | description: 'Whether to enable page view' 64 | }, 65 | inbox_view: { 66 | type: 'boolean', 67 | description: 'Whether to enable inbox view' 68 | } 69 | }, 70 | required: ['name', 'identifier'] 71 | }; 72 | 73 | constructor(private client: PlaneApiClient) {} 74 | 75 | async execute(args: Record<string, unknown>): Promise<ToolResponse> { 76 | try { 77 | const input = zodInputSchema.parse(args); 78 | const { workspace_slug, ...projectData } = input; 79 | const workspace = workspace_slug || this.client.instance.defaultWorkspace; 80 | 81 | this.client.notify({ 82 | type: 'info', 83 | message: `Creating project "${projectData.name}" in workspace: ${workspace}`, 84 | source: this.name, 85 | data: { 86 | workspace, 87 | ...projectData 88 | } 89 | }); 90 | 91 | try { 92 | const project = await this.client.createProject(workspace, projectData); 93 | 94 | this.client.notify({ 95 | type: 'success', 96 | message: `Successfully created project "${project.name}" (ID: ${project.id}) in workspace "${workspace}"`, 97 | source: this.name, 98 | data: { 99 | workspace, 100 | projectId: project.id 101 | } 102 | }); 103 | 104 | return { 105 | isError: false, 106 | content: [{ 107 | type: 'text', 108 | text: `Successfully created project "${project.name}" (ID: ${project.id}) in workspace "${workspace}"` 109 | }] 110 | }; 111 | } catch (error) { 112 | const errorMessage = error instanceof Error ? error.message : 'Unknown error'; 113 | this.client.notify({ 114 | type: 'error', 115 | message: `Failed to create project: ${errorMessage}`, 116 | source: this.name, 117 | data: { 118 | error: errorMessage 119 | } 120 | }); 121 | 122 | return { 123 | isError: true, 124 | content: [{ 125 | type: 'text', 126 | text: `Failed to create project: ${errorMessage}` 127 | }] 128 | }; 129 | } 130 | } catch (error) { 131 | if (error instanceof z.ZodError) { 132 | throw error; 133 | } 134 | const errorMessage = error instanceof Error ? error.message : 'Unknown error'; 135 | return { 136 | isError: true, 137 | content: [{ 138 | type: 'text', 139 | text: `Failed to create project: ${errorMessage}` 140 | }] 141 | }; 142 | } 143 | } 144 | } 145 | ``` -------------------------------------------------------------------------------- /src/tools/projects/__tests__/update.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { describe, it, expect, beforeEach, vi } from 'vitest'; 2 | import { UpdateProjectTool } from '../update.js'; 3 | import { PlaneApiClient } from '../../../api/client.js'; 4 | import { PlaneInstance } from '../../../config/plane-config.js'; 5 | 6 | // Mock the PlaneApiClient 7 | vi.mock('../../../api/client.js', () => { 8 | return { 9 | PlaneApiClient: vi.fn().mockImplementation((instance, context) => ({ 10 | instance, 11 | updateProject: vi.fn(), 12 | notify: vi.fn() 13 | })) 14 | }; 15 | }); 16 | 17 | describe('UpdateProjectTool', () => { 18 | let tool: UpdateProjectTool; 19 | let mockClient: PlaneApiClient; 20 | 21 | beforeEach(() => { 22 | // Create a mock instance 23 | const mockInstance: PlaneInstance = { 24 | name: 'test', 25 | baseUrl: 'https://test.plane.so', 26 | apiKey: 'test-key', 27 | defaultWorkspace: 'test-workspace' 28 | }; 29 | 30 | // Create a mock context 31 | const mockContext = { 32 | progressToken: '123', 33 | workspace: 'test-workspace' 34 | }; 35 | 36 | // Create a mock client 37 | mockClient = new PlaneApiClient(mockInstance, mockContext); 38 | 39 | // Create the tool instance 40 | tool = new UpdateProjectTool(mockClient); 41 | }); 42 | 43 | it('should update a project with minimal fields', async () => { 44 | const mockProject = { 45 | id: 'test-id', 46 | name: 'Updated Project', 47 | identifier: 'TEST' 48 | }; 49 | 50 | (mockClient.updateProject as any).mockResolvedValue(mockProject); 51 | 52 | const result = await tool.execute({ 53 | project_id: 'test-id', 54 | name: 'Updated Project' 55 | }); 56 | 57 | expect(mockClient.updateProject).toHaveBeenCalledWith('test-workspace', 'test-id', { 58 | name: 'Updated Project' 59 | }); 60 | 61 | expect(result.content[0].text).toContain('Successfully updated project'); 62 | expect(result.content[0].text).toContain(mockProject.id); 63 | }); 64 | 65 | it('should update a project with all optional fields', async () => { 66 | const mockProject = { 67 | id: 'test-id', 68 | name: 'Updated Project', 69 | identifier: 'TEST', 70 | description: 'Updated Description', 71 | network: 2, 72 | emoji: '1f680', 73 | module_view: false, 74 | cycle_view: false, 75 | issue_views_view: false, 76 | page_view: false, 77 | inbox_view: true 78 | }; 79 | 80 | (mockClient.updateProject as any).mockResolvedValue(mockProject); 81 | 82 | const result = await tool.execute({ 83 | project_id: 'test-id', 84 | name: 'Updated Project', 85 | identifier: 'TEST', 86 | description: 'Updated Description', 87 | network: 2, 88 | emoji: '1f680', 89 | module_view: false, 90 | cycle_view: false, 91 | issue_views_view: false, 92 | page_view: false, 93 | inbox_view: true 94 | }); 95 | 96 | expect(mockClient.updateProject).toHaveBeenCalledWith('test-workspace', 'test-id', { 97 | name: 'Updated Project', 98 | identifier: 'TEST', 99 | description: 'Updated Description', 100 | network: 2, 101 | emoji: '1f680', 102 | module_view: false, 103 | cycle_view: false, 104 | issue_views_view: false, 105 | page_view: false, 106 | inbox_view: true 107 | }); 108 | 109 | expect(result.content[0].text).toContain('Successfully updated project'); 110 | expect(result.content[0].text).toContain(mockProject.id); 111 | }); 112 | 113 | it('should use provided workspace instead of default', async () => { 114 | const mockProject = { 115 | id: 'test-id', 116 | name: 'Updated Project' 117 | }; 118 | 119 | (mockClient.updateProject as any).mockResolvedValue(mockProject); 120 | 121 | await tool.execute({ 122 | workspace_slug: 'custom-workspace', 123 | project_id: 'test-id', 124 | name: 'Updated Project' 125 | }); 126 | 127 | expect(mockClient.updateProject).toHaveBeenCalledWith('custom-workspace', 'test-id', { 128 | name: 'Updated Project' 129 | }); 130 | }); 131 | 132 | it('should handle API errors', async () => { 133 | const errorMessage = 'API Error: Project update failed'; 134 | (mockClient.updateProject as any).mockRejectedValue(new Error(errorMessage)); 135 | 136 | const result = await tool.execute({ 137 | project_id: 'test-id', 138 | name: 'Updated Project' 139 | }); 140 | 141 | expect(result.isError).toBe(true); 142 | expect(result.content[0].text).toContain(errorMessage); 143 | expect(mockClient.notify).toHaveBeenCalledWith(expect.objectContaining({ 144 | type: 'error', 145 | message: expect.stringContaining('Failed to update project') 146 | })); 147 | }); 148 | 149 | it('should validate required fields', async () => { 150 | await expect(tool.execute({ 151 | name: 'Updated Project' 152 | // Missing project_id 153 | })).rejects.toThrow(); 154 | }); 155 | 156 | it('should validate field types', async () => { 157 | await expect(tool.execute({ 158 | project_id: 'test-id', 159 | network: 3 // Invalid network value 160 | })).rejects.toThrow(); 161 | 162 | await expect(tool.execute({ 163 | project_id: 'test-id', 164 | archive_in: 13 // Invalid archive_in value 165 | })).rejects.toThrow(); 166 | }); 167 | }); ``` -------------------------------------------------------------------------------- /src/tools/issues/update.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { Tool, ToolResponse } from '../../types/mcp.js'; 2 | import { IssuesClient } from '../../api/issues/client.js'; 3 | import { UpdateIssueData, IssuePriority } from '../../api/issues/types.js'; 4 | import { PlaneInstanceConfig } from '../../api/types/config.js'; 5 | 6 | interface UpdateIssueInput extends UpdateIssueData { 7 | workspace_slug?: string; 8 | project_id: string; 9 | issue_id: string; 10 | } 11 | 12 | export class UpdateIssueTool implements Tool { 13 | private issuesClient: IssuesClient; 14 | private instance: PlaneInstanceConfig; 15 | 16 | name = 'claudeus_plane_issues__update'; 17 | description = 'Updates an existing issue in a Plane project'; 18 | status = 'enabled' as const; 19 | inputSchema = { 20 | type: 'object', 21 | properties: { 22 | workspace_slug: { 23 | type: 'string', 24 | description: 'The slug of the workspace containing the issue. If not provided, uses the default workspace.' 25 | }, 26 | project_id: { 27 | type: 'string', 28 | description: 'The ID of the project containing the issue' 29 | }, 30 | issue_id: { 31 | type: 'string', 32 | description: 'The ID of the issue to update' 33 | }, 34 | name: { 35 | type: 'string', 36 | description: 'The new name/title of the issue' 37 | }, 38 | description_html: { 39 | type: 'string', 40 | description: 'The new HTML description of the issue' 41 | }, 42 | priority: { 43 | type: 'string', 44 | enum: ['urgent', 'high', 'medium', 'low', 'none'], 45 | description: 'The new priority of the issue' 46 | }, 47 | start_date: { 48 | type: 'string', 49 | format: 'date', 50 | description: 'The new start date of the issue (YYYY-MM-DD)' 51 | }, 52 | target_date: { 53 | type: 'string', 54 | format: 'date', 55 | description: 'The new target date of the issue (YYYY-MM-DD)' 56 | }, 57 | estimate_point: { 58 | type: 'number', 59 | description: 'The new story points or time estimate for the issue' 60 | }, 61 | state: { 62 | type: 'string', 63 | description: 'The new state ID for the issue' 64 | }, 65 | assignees: { 66 | type: 'array', 67 | items: { 68 | type: 'string' 69 | }, 70 | description: 'New array of user IDs to assign to the issue' 71 | }, 72 | labels: { 73 | type: 'array', 74 | items: { 75 | type: 'string' 76 | }, 77 | description: 'New array of label IDs to apply to the issue' 78 | }, 79 | parent: { 80 | type: 'string', 81 | description: 'New parent issue ID (for sub-issues)' 82 | }, 83 | is_draft: { 84 | type: 'boolean', 85 | description: 'Whether this issue should be marked as draft' 86 | }, 87 | archived_at: { 88 | type: 'string', 89 | format: 'date-time', 90 | description: 'When to archive the issue (ISO 8601 format)' 91 | }, 92 | completed_at: { 93 | type: 'string', 94 | format: 'date-time', 95 | description: 'When the issue was completed (ISO 8601 format)' 96 | } 97 | }, 98 | required: ['project_id', 'issue_id'] 99 | }; 100 | 101 | constructor(instance: PlaneInstanceConfig) { 102 | this.instance = instance; 103 | this.issuesClient = new IssuesClient(this.instance); 104 | } 105 | 106 | async execute(args: Record<string, unknown>): Promise<ToolResponse> { 107 | // Type cast with validation 108 | const input = args as unknown as UpdateIssueInput; 109 | if (!this.validateInput(input)) { 110 | return { 111 | isError: true, 112 | content: [{ 113 | type: 'text', 114 | text: 'Invalid input: missing required fields' 115 | }] 116 | }; 117 | } 118 | 119 | const { 120 | workspace_slug = this.instance.defaultWorkspace, 121 | project_id, 122 | issue_id, 123 | ...updateData 124 | } = input; 125 | 126 | // Validate workspace 127 | if (!workspace_slug) { 128 | return { 129 | isError: true, 130 | content: [{ 131 | type: 'text', 132 | text: 'Workspace slug is required' 133 | }] 134 | }; 135 | } 136 | 137 | // Validate project ID 138 | if (!project_id) { 139 | return { 140 | isError: true, 141 | content: [{ 142 | type: 'text', 143 | text: 'Project ID is required' 144 | }] 145 | }; 146 | } 147 | 148 | // Validate issue ID 149 | if (!issue_id) { 150 | return { 151 | isError: true, 152 | content: [{ 153 | type: 'text', 154 | text: 'Issue ID is required' 155 | }] 156 | }; 157 | } 158 | 159 | // Validate that at least one field is being updated 160 | if (Object.keys(updateData).length === 0) { 161 | return { 162 | isError: true, 163 | content: [{ 164 | type: 'text', 165 | text: 'At least one field must be provided for update' 166 | }] 167 | }; 168 | } 169 | 170 | try { 171 | const response = await this.issuesClient.update( 172 | workspace_slug, 173 | project_id, 174 | issue_id, 175 | updateData 176 | ); 177 | 178 | return { 179 | content: [{ 180 | type: 'text', 181 | text: JSON.stringify(response) 182 | }] 183 | }; 184 | } catch (error: unknown) { 185 | const errorMessage = error instanceof Error ? error.message : 'Unknown error'; 186 | return { 187 | isError: true, 188 | content: [{ 189 | type: 'text', 190 | text: `Failed to update issue: ${errorMessage}` 191 | }] 192 | }; 193 | } 194 | } 195 | 196 | private validateInput(input: unknown): input is UpdateIssueInput { 197 | if (typeof input !== 'object' || input === null) return false; 198 | const data = input as Record<string, unknown>; 199 | return typeof data.project_id === 'string' && typeof data.issue_id === 'string'; 200 | } 201 | } ``` -------------------------------------------------------------------------------- /src/tools/projects/__tests__/create.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { describe, it, expect, beforeEach, vi } from 'vitest'; 2 | import { CreateProjectTool } from '../create.js'; 3 | import { PlaneApiClient } from '../../../api/client.js'; 4 | import { PlaneInstance } from '../../../config/plane-config.js'; 5 | 6 | // Mock the PlaneApiClient 7 | vi.mock('../../../api/client.js', () => { 8 | return { 9 | PlaneApiClient: vi.fn().mockImplementation((instance, context) => ({ 10 | instance, 11 | createProject: vi.fn(), 12 | notify: vi.fn() 13 | })) 14 | }; 15 | }); 16 | 17 | describe('CreateProjectTool', () => { 18 | let tool: CreateProjectTool; 19 | let mockClient: PlaneApiClient; 20 | 21 | beforeEach(() => { 22 | // Create a mock instance 23 | const mockInstance: PlaneInstance = { 24 | name: 'test', 25 | baseUrl: 'https://test.plane.so', 26 | apiKey: 'test-key', 27 | defaultWorkspace: 'test-workspace' 28 | }; 29 | 30 | // Create a mock context 31 | const mockContext = { 32 | progressToken: '123', 33 | workspace: 'test-workspace' 34 | }; 35 | 36 | // Create a mock client 37 | mockClient = new PlaneApiClient(mockInstance, mockContext); 38 | 39 | // Create the tool instance 40 | tool = new CreateProjectTool(mockClient); 41 | }); 42 | 43 | it('should create a project with minimal required fields', async () => { 44 | const mockProject = { 45 | id: 'test-id', 46 | name: 'Test Project', 47 | identifier: 'TEST' 48 | }; 49 | 50 | (mockClient.createProject as any).mockResolvedValue(mockProject); 51 | 52 | const result = await tool.execute({ 53 | name: 'Test Project', 54 | identifier: 'TEST' 55 | }); 56 | 57 | expect(mockClient.createProject).toHaveBeenCalledWith('test-workspace', { 58 | name: 'Test Project', 59 | identifier: 'TEST' 60 | }); 61 | 62 | expect(result.content[0].text).toContain('Successfully created project'); 63 | expect(result.content[0].text).toContain(mockProject.id); 64 | }); 65 | 66 | it('should create a project with all optional fields', async () => { 67 | const mockProject = { 68 | id: 'test-id', 69 | name: 'Test Project', 70 | identifier: 'TEST', 71 | description: 'Test Description', 72 | network: 2, 73 | emoji: '1f680', 74 | module_view: true, 75 | cycle_view: true, 76 | issue_views_view: true, 77 | page_view: true, 78 | inbox_view: false 79 | }; 80 | 81 | (mockClient.createProject as any).mockResolvedValue(mockProject); 82 | 83 | const result = await tool.execute({ 84 | name: 'Test Project', 85 | identifier: 'TEST', 86 | description: 'Test Description', 87 | network: 2, 88 | emoji: '1f680', 89 | module_view: true, 90 | cycle_view: true, 91 | issue_views_view: true, 92 | page_view: true, 93 | inbox_view: false 94 | }); 95 | 96 | expect(mockClient.createProject).toHaveBeenCalledWith('test-workspace', { 97 | name: 'Test Project', 98 | identifier: 'TEST', 99 | description: 'Test Description', 100 | network: 2, 101 | emoji: '1f680', 102 | module_view: true, 103 | cycle_view: true, 104 | issue_views_view: true, 105 | page_view: true, 106 | inbox_view: false 107 | }); 108 | 109 | expect(result.content[0].text).toContain('Successfully created project'); 110 | expect(result.content[0].text).toContain(mockProject.id); 111 | }); 112 | 113 | it('should use provided workspace instead of default', async () => { 114 | const mockProject = { 115 | id: 'test-id', 116 | name: 'Test Project', 117 | identifier: 'TEST' 118 | }; 119 | 120 | (mockClient.createProject as any).mockResolvedValue(mockProject); 121 | 122 | await tool.execute({ 123 | workspace_slug: 'custom-workspace', 124 | name: 'Test Project', 125 | identifier: 'TEST' 126 | }); 127 | 128 | expect(mockClient.createProject).toHaveBeenCalledWith('custom-workspace', { 129 | name: 'Test Project', 130 | identifier: 'TEST' 131 | }); 132 | }); 133 | 134 | it('should handle API errors', async () => { 135 | const errorMessage = 'API Error: Project creation failed'; 136 | (mockClient.createProject as any).mockRejectedValue(new Error(errorMessage)); 137 | 138 | const result = await tool.execute({ 139 | name: 'Test Project', 140 | identifier: 'TEST' 141 | }); 142 | 143 | expect(result.isError).toBe(true); 144 | expect(result.content[0].text).toContain(errorMessage); 145 | expect(mockClient.notify).toHaveBeenCalledWith(expect.objectContaining({ 146 | type: 'error', 147 | message: expect.stringContaining('Failed to create project') 148 | })); 149 | }); 150 | 151 | it('should validate required fields', async () => { 152 | await expect(tool.execute({ 153 | name: 'Test Project' 154 | // Missing identifier 155 | })).rejects.toThrow(); 156 | 157 | await expect(tool.execute({ 158 | identifier: 'TEST' 159 | // Missing name 160 | })).rejects.toThrow(); 161 | }); 162 | 163 | it('should validate field types', async () => { 164 | await expect(tool.execute({ 165 | name: 'Test Project', 166 | identifier: 'TEST', 167 | network: 3 // Invalid network value 168 | })).rejects.toThrow(); 169 | 170 | await expect(tool.execute({ 171 | name: 'Test Project', 172 | identifier: 'TEST', 173 | archive_in: 13 // Invalid archive_in value 174 | })).rejects.toThrow(); 175 | }); 176 | }); 177 | ``` -------------------------------------------------------------------------------- /src/test/mcp-test-harness.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { EventEmitter } from 'events'; 2 | import dummyProjects from '@/dummy-data/projects.json' assert { type: 'json' }; 3 | 4 | export interface MCPContentItem { 5 | type: string; 6 | text: string; 7 | } 8 | 9 | export interface MCPMessage { 10 | jsonrpc: '2.0'; 11 | id: number; 12 | method?: string; 13 | params?: Record<string, unknown>; 14 | result?: { 15 | content?: MCPContentItem[]; 16 | capabilities?: Record<string, unknown>; 17 | [key: string]: unknown; 18 | }; 19 | error?: { 20 | code: number; 21 | message: string; 22 | data?: unknown; 23 | }; 24 | } 25 | 26 | interface Transport { 27 | onMessage: (handler: (message: MCPMessage) => void) => void; 28 | send: (message: MCPMessage) => void; 29 | } 30 | 31 | class InMemoryTransport implements Transport { 32 | private messageHandler?: (message: MCPMessage) => void; 33 | private otherTransport?: InMemoryTransport; 34 | 35 | onMessage(handler: (message: MCPMessage) => void): void { 36 | this.messageHandler = handler; 37 | } 38 | 39 | send(message: MCPMessage): void { 40 | if (this.otherTransport?.messageHandler) { 41 | this.otherTransport.messageHandler(message); 42 | } 43 | } 44 | 45 | static createLinkedPair(): [InMemoryTransport, InMemoryTransport] { 46 | const client = new InMemoryTransport(); 47 | const server = new InMemoryTransport(); 48 | client.otherTransport = server; 49 | server.otherTransport = client; 50 | return [client, server]; 51 | } 52 | } 53 | 54 | export class MCPTestHarness { 55 | private clientTransport: InMemoryTransport; 56 | private serverTransport: InMemoryTransport; 57 | private responseHandlers = new Map<number, (response: MCPMessage) => void>(); 58 | private isConnected = false; 59 | private nextMessageId = 1; 60 | 61 | constructor() { 62 | [this.clientTransport, this.serverTransport] = InMemoryTransport.createLinkedPair(); 63 | 64 | // Set up server-side message handling 65 | this.serverTransport.onMessage((message: MCPMessage) => { 66 | if (message.method === 'initialize') { 67 | this.handleInitialize(message); 68 | } else if (message.method === 'tool/call') { 69 | this.handleToolCall(message); 70 | } 71 | }); 72 | 73 | // Set up client-side message handling 74 | this.clientTransport.onMessage((message: MCPMessage) => { 75 | if (message.id && this.responseHandlers.has(message.id)) { 76 | const handler = this.responseHandlers.get(message.id)!; 77 | handler(message); 78 | } 79 | }); 80 | } 81 | 82 | private handleInitialize(message: MCPMessage): void { 83 | if (!message.id) { 84 | return; 85 | } 86 | 87 | this.serverTransport.send({ 88 | jsonrpc: '2.0', 89 | id: message.id, 90 | result: { 91 | capabilities: { 92 | sampling: {}, 93 | roots: { listChanged: true } 94 | } 95 | } 96 | }); 97 | } 98 | 99 | private handleToolCall(message: MCPMessage): void { 100 | if (!message.id || !message.params) { 101 | return; 102 | } 103 | 104 | const { name, args } = message.params as { name: string; args: Record<string, unknown> }; 105 | 106 | if (name === 'claudeus_plane_projects__list') { 107 | if (!args.workspace) { 108 | this.serverTransport.send({ 109 | jsonrpc: '2.0', 110 | id: message.id, 111 | result: { 112 | content: [{ 113 | type: 'text', 114 | text: 'Error: Missing required workspace parameter' 115 | }] 116 | } 117 | }); 118 | return; 119 | } 120 | 121 | if (args.workspace === 'invalid-workspace-id') { 122 | this.serverTransport.send({ 123 | jsonrpc: '2.0', 124 | id: message.id, 125 | result: { 126 | content: [{ 127 | type: 'text', 128 | text: 'Error: Invalid workspace ID' 129 | }] 130 | } 131 | }); 132 | return; 133 | } 134 | 135 | // Return actual dummy projects for valid workspace 136 | this.serverTransport.send({ 137 | jsonrpc: '2.0', 138 | id: message.id, 139 | result: { 140 | content: [{ 141 | type: 'text', 142 | text: JSON.stringify(dummyProjects) 143 | }] 144 | } 145 | }); 146 | } 147 | } 148 | 149 | async connect(): Promise<MCPMessage> { 150 | if (this.isConnected) { 151 | throw new Error('Already connected'); 152 | } 153 | this.isConnected = true; 154 | return this.sendInitialize(); 155 | } 156 | 157 | async sendInitialize(): Promise<MCPMessage> { 158 | const initMessage: MCPMessage = { 159 | jsonrpc: '2.0', 160 | id: this.nextMessageId++, 161 | method: 'initialize', 162 | params: { 163 | capabilities: { 164 | sampling: {}, 165 | roots: { listChanged: true } 166 | } 167 | } 168 | }; 169 | 170 | return this.sendMessage(initMessage); 171 | } 172 | 173 | async sendMessage(message: MCPMessage): Promise<MCPMessage> { 174 | if (!this.isConnected) { 175 | throw new Error('Not connected'); 176 | } 177 | 178 | if (!message.id) { 179 | throw new Error('Message must have an ID'); 180 | } 181 | 182 | return new Promise((resolve, reject) => { 183 | const timeout = setTimeout(() => { 184 | this.responseHandlers.delete(message.id!); 185 | reject(new Error('Message timeout')); 186 | }, 5000); 187 | 188 | this.responseHandlers.set(message.id, (response: MCPMessage) => { 189 | clearTimeout(timeout); 190 | this.responseHandlers.delete(message.id!); 191 | resolve(response); 192 | }); 193 | 194 | this.clientTransport.send(message); 195 | }); 196 | } 197 | 198 | async callTool(name: string, args: Record<string, unknown>): Promise<MCPMessage> { 199 | const message: MCPMessage = { 200 | jsonrpc: '2.0', 201 | id: this.nextMessageId++, 202 | method: 'tool/call', 203 | params: { 204 | name, 205 | args 206 | } 207 | }; 208 | 209 | return this.sendMessage(message); 210 | } 211 | 212 | onServerMessage(message: MCPMessage): void { 213 | this.serverTransport.send(message); 214 | } 215 | 216 | clearHandlers(): void { 217 | this.responseHandlers.clear(); 218 | this.isConnected = false; 219 | } 220 | } ``` -------------------------------------------------------------------------------- /src/dummy-data/projects.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "grouped_by": null, 3 | "sub_grouped_by": null, 4 | "total_count": 3, 5 | "next_cursor": "1000:1:0", 6 | "prev_cursor": "1000:-1:1", 7 | "next_page_results": false, 8 | "prev_page_results": false, 9 | "count": 3, 10 | "total_pages": 1, 11 | "total_results": 3, 12 | "extra_stats": null, 13 | "results": [ 14 | { 15 | "id": "01234567-89ab-cdef-0123-456789abcdef", 16 | "total_members": 3, 17 | "total_cycles": 2, 18 | "total_modules": 1, 19 | "is_member": true, 20 | "sort_order": 65535, 21 | "member_role": 20, 22 | "is_deployed": true, 23 | "cover_image_url": "https://images.unsplash.com/photo-1234567890", 24 | "inbox_view": true, 25 | "created_at": "2024-01-01T12:00:00.000000+01:00", 26 | "updated_at": "2024-01-02T12:00:00.000000+01:00", 27 | "deleted_at": null, 28 | "name": "Example Project", 29 | "description": "This is an example project description that demonstrates the format of project data in the Plane API. It includes various fields and properties that would typically be associated with a project.", 30 | "description_text": null, 31 | "description_html": null, 32 | "network": 1, 33 | "identifier": "EXAMPLE", 34 | "emoji": "📝", 35 | "icon_prop": null, 36 | "module_view": true, 37 | "cycle_view": true, 38 | "issue_views_view": true, 39 | "page_view": true, 40 | "intake_view": true, 41 | "is_time_tracking_enabled": true, 42 | "is_issue_type_enabled": true, 43 | "guest_view_all_features": false, 44 | "cover_image": "https://images.unsplash.com/photo-1234567890", 45 | "archive_in": 0, 46 | "close_in": 0, 47 | "logo_props": { 48 | "icon": { 49 | "name": "document", 50 | "color": "#4a90e2" 51 | }, 52 | "in_use": "icon" 53 | }, 54 | "archived_at": null, 55 | "timezone": "UTC", 56 | "created_by": "00000000-0000-0000-0000-000000000001", 57 | "updated_by": "00000000-0000-0000-0000-000000000001", 58 | "workspace": "00000000-0000-0000-0000-000000000002", 59 | "default_assignee": "00000000-0000-0000-0000-000000000001", 60 | "project_lead": "00000000-0000-0000-0000-000000000001", 61 | "cover_image_asset": null, 62 | "estimate": "00000000-0000-0000-0000-000000000003", 63 | "default_state": null 64 | }, 65 | { 66 | "id": "1cd54b31-bc91-5747-b2b6-c7588ddc76c5", 67 | "total_members": 3, 68 | "total_cycles": 2, 69 | "total_modules": 2, 70 | "is_member": true, 71 | "sort_order": 65534, 72 | "member_role": 20, 73 | "is_deployed": true, 74 | "cover_image_url": "https://images.unsplash.com/photo-1460925895917-afdab827c52f?auto=format&fit=crop&q=80&ixlib=rb-4.0.3", 75 | "inbox_view": true, 76 | "created_at": "2025-01-15T10:15:33.224935+01:00", 77 | "updated_at": "2025-01-24T16:45:12.445123+01:00", 78 | "deleted_at": null, 79 | "name": "MCP Framework", 80 | "description": "Development of the Multi-Client Protocol (MCP) Framework for standardized API integrations across all SimHop services. This framework will serve as the foundation for all our future client integrations and internal tools.", 81 | "description_text": null, 82 | "description_html": null, 83 | "network": 1, 84 | "identifier": "MCP", 85 | "emoji": "🔌", 86 | "icon_prop": null, 87 | "module_view": true, 88 | "cycle_view": true, 89 | "issue_views_view": true, 90 | "page_view": true, 91 | "intake_view": true, 92 | "is_time_tracking_enabled": true, 93 | "is_issue_type_enabled": true, 94 | "guest_view_all_features": false, 95 | "cover_image": "https://images.unsplash.com/photo-1460925895917-afdab827c52f?auto=format&fit=crop&q=80&ixlib=rb-4.0.3", 96 | "archive_in": 0, 97 | "close_in": 0, 98 | "logo_props": { 99 | "icon": { 100 | "name": "plug", 101 | "color": "#3366ff" 102 | }, 103 | "in_use": "icon" 104 | }, 105 | "archived_at": null, 106 | "timezone": "UTC", 107 | "created_by": "52ab338c-2239-48fe-8e18-588bb17a78fc", 108 | "updated_by": "52ab338c-2239-48fe-8e18-588bb17a78fc", 109 | "workspace": "6bb6e42b-0bb7-43a2-b561-677dc52df44f", 110 | "default_assignee": "52ab338c-2239-48fe-8e18-588bb17a78fc", 111 | "project_lead": "52ab338c-2239-48fe-8e18-588bb17a78fc", 112 | "cover_image_asset": null, 113 | "estimate": "146ba6b4-a645-49a5-a57f-59800f5a8cd6", 114 | "default_state": null 115 | }, 116 | { 117 | "id": "2ef65c42-cd92-6858-c3c7-d8699eed87d6", 118 | "total_members": 2, 119 | "total_cycles": 3, 120 | "total_modules": 1, 121 | "is_member": true, 122 | "sort_order": 65533, 123 | "member_role": 20, 124 | "is_deployed": false, 125 | "cover_image_url": "https://images.unsplash.com/photo-1551288049-bebda4e38f71?auto=format&fit=crop&q=80&ixlib=rb-4.0.3", 126 | "inbox_view": true, 127 | "created_at": "2025-01-20T14:30:45.334123+01:00", 128 | "updated_at": "2025-01-25T09:15:22.556789+01:00", 129 | "deleted_at": null, 130 | "name": "AI Assistant Hub", 131 | "description": "Creation of an AI-powered assistant hub that integrates with our existing tools and services. This project focuses on developing intelligent automation and support features to enhance productivity across all SimHop operations.", 132 | "description_text": null, 133 | "description_html": null, 134 | "network": 3, 135 | "identifier": "AIH", 136 | "emoji": "🤖", 137 | "icon_prop": null, 138 | "module_view": true, 139 | "cycle_view": true, 140 | "issue_views_view": true, 141 | "page_view": true, 142 | "intake_view": true, 143 | "is_time_tracking_enabled": true, 144 | "is_issue_type_enabled": true, 145 | "guest_view_all_features": false, 146 | "cover_image": "https://images.unsplash.com/photo-1551288049-bebda4e38f71?auto=format&fit=crop&q=80&ixlib=rb-4.0.3", 147 | "archive_in": 0, 148 | "close_in": 0, 149 | "logo_props": { 150 | "icon": { 151 | "name": "robot", 152 | "color": "#00cc88" 153 | }, 154 | "in_use": "icon" 155 | }, 156 | "archived_at": null, 157 | "timezone": "UTC", 158 | "created_by": "52ab338c-2239-48fe-8e18-588bb17a78fc", 159 | "updated_by": "52ab338c-2239-48fe-8e18-588bb17a78fc", 160 | "workspace": "6bb6e42b-0bb7-43a2-b561-677dc52df44f", 161 | "default_assignee": "52ab338c-2239-48fe-8e18-588bb17a78fc", 162 | "project_lead": "52ab338c-2239-48fe-8e18-588bb17a78fc", 163 | "cover_image_asset": null, 164 | "estimate": "146ba6b4-a645-49a5-a57f-59800f5a8cd6", 165 | "default_state": null 166 | } 167 | ] 168 | } ```