#
tokens: 48638/50000 58/65 files (page 1/2)
lines: on (toggle) GitHub
raw markdown copy reset
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 | ![License](https://img.shields.io/badge/license-MIT-blue.svg)
  9 | ![Node](https://img.shields.io/badge/node-%3E%3D22.0.0-brightgreen.svg)
 10 | [![GitHub Stars](https://img.shields.io/github/stars/deus-h/claudeus-plane-mcp.svg)](https://github.com/deus-h/claudeus-plane-mcp/stargazers)
 11 | [![NPM Version](https://img.shields.io/npm/v/claudeus-plane-mcp.svg)](https://www.npmjs.com/package/claudeus-plane-mcp)
 12 | [![NPM Downloads](https://img.shields.io/npm/dm/claudeus-plane-mcp.svg)](https://www.npmjs.com/package/claudeus-plane-mcp)
 13 | [![GitHub Discussions](https://img.shields.io/github/discussions/deus-h/claudeus-plane-mcp.svg)](https://github.com/deus-h/claudeus-plane-mcp/discussions)
 14 | [![GitHub Forks](https://img.shields.io/github/forks/deus-h/claudeus-plane-mcp.svg)](https://github.com/deus-h/claudeus-plane-mcp/network)
 15 | [![smithery badge](https://smithery.ai/badge/claudeus-plane-mcp)](https://smithery.ai/server/claudeus-plane-mcp)
 16 | [![MCP Standard](https://img.shields.io/badge/MCP-2024--11--05-purple.svg)](https://github.com/deus-h/claudeus-plane-mcp)
 17 | [![Test Coverage](https://img.shields.io/badge/coverage-95%25-brightgreen.svg)](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 | | &nbsp; | &nbsp; | &nbsp; |
444 | | **Claude Pro** | **<span style="color: #A351D6">$20/month</span>** | At [Anthropic](https://claude.ai/settings/billing?action=subscribe) |
445 | | &nbsp; | &nbsp; | &nbsp; |
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 |   }
```
Page 1/2FirstPrevNextLast