backlog-mcp 1.0.3__tar.gz → 1.0.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (24) hide show
  1. {backlog_mcp-1.0.3 → backlog_mcp-1.0.4}/PKG-INFO +1 -1
  2. {backlog_mcp-1.0.3 → backlog_mcp-1.0.4}/app/tools/get_issue_details.py +4 -1
  3. {backlog_mcp-1.0.3 → backlog_mcp-1.0.4}/app/utils/ultils.py +38 -29
  4. {backlog_mcp-1.0.3 → backlog_mcp-1.0.4}/pyproject.toml +1 -1
  5. backlog_mcp-1.0.4/scripts/install-backlog-mcp.sh +702 -0
  6. {backlog_mcp-1.0.3 → backlog_mcp-1.0.4}/uv.lock +1 -1
  7. {backlog_mcp-1.0.3 → backlog_mcp-1.0.4}/.env.example +0 -0
  8. {backlog_mcp-1.0.3 → backlog_mcp-1.0.4}/.gitignore +0 -0
  9. {backlog_mcp-1.0.3 → backlog_mcp-1.0.4}/.mise.toml +0 -0
  10. {backlog_mcp-1.0.3 → backlog_mcp-1.0.4}/README.md +0 -0
  11. {backlog_mcp-1.0.3 → backlog_mcp-1.0.4}/app/__init__.py +0 -0
  12. {backlog_mcp-1.0.3 → backlog_mcp-1.0.4}/app/constants/__init__.py +0 -0
  13. {backlog_mcp-1.0.3 → backlog_mcp-1.0.4}/app/constants/constants.py +0 -0
  14. {backlog_mcp-1.0.3 → backlog_mcp-1.0.4}/app/core/__init__.py +0 -0
  15. {backlog_mcp-1.0.3 → backlog_mcp-1.0.4}/app/logging_config.py +0 -0
  16. {backlog_mcp-1.0.3 → backlog_mcp-1.0.4}/app/main.py +0 -0
  17. {backlog_mcp-1.0.3 → backlog_mcp-1.0.4}/app/models/__init__.py +0 -0
  18. {backlog_mcp-1.0.3 → backlog_mcp-1.0.4}/app/models/models.py +0 -0
  19. {backlog_mcp-1.0.3 → backlog_mcp-1.0.4}/app/server_settings.py +0 -0
  20. {backlog_mcp-1.0.3 → backlog_mcp-1.0.4}/app/tools/__init__.py +0 -0
  21. {backlog_mcp-1.0.3 → backlog_mcp-1.0.4}/app/tools/get_user_issue_list.py +0 -0
  22. {backlog_mcp-1.0.3 → backlog_mcp-1.0.4}/app/utils/__init__.py +0 -0
  23. {backlog_mcp-1.0.3 → backlog_mcp-1.0.4}/app/utils/di.py +0 -0
  24. {backlog_mcp-1.0.3 → backlog_mcp-1.0.4}/docs/mcp-installer-template.md +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: backlog-mcp
3
- Version: 1.0.3
3
+ Version: 1.0.4
4
4
  Summary: A Model Context Protocol (MCP) server for Backlog project management integration
5
5
  Author-email: BaoNguyen <baonguyen@teqnological.asia>
6
6
  Requires-Python: <3.14,>=3.13
@@ -4,13 +4,15 @@ from app.utils.ultils import get_issue_detail_handler
4
4
 
5
5
  async def get_issue_details(
6
6
  issue_key: str,
7
- timezone: str = "UTC"
7
+ include_comments: bool,
8
+ timezone: str = "UTC",
8
9
  ):
9
10
  """
10
11
  Get details of a Backlog issue by its key.
11
12
 
12
13
  Args:
13
14
  issue_key (str): The key of the Backlog issue to retrieve.
15
+ include_comments (bool): Whether to include comments in the response.
14
16
  timezone (str, optional): The timezone to format datetime fields. Defaults to "UTC".
15
17
  """
16
18
  try:
@@ -22,6 +24,7 @@ async def get_issue_details(
22
24
  api_key=ctx.api_key,
23
25
  issue_key=issue_key,
24
26
  timezone=timezone,
27
+ include_comments=include_comments,
25
28
  )
26
29
  return result
27
30
  except Exception as e:
@@ -46,28 +46,29 @@ def time_in_range(time: str, start_range: str, end_range: str):
46
46
  return start_range_time <= time_to_be_compared <= end_range_time
47
47
 
48
48
 
49
- def process_issue_detail(issue_detail, timezone, issue_key):
49
+ def process_issue_detail(issue_detail, timezone, issue_key, include_comments: bool = True):
50
50
  processed_issue = {
51
51
  "issue_key": issue_key,
52
52
  "summary": issue_detail["summary"],
53
53
  "description": issue_detail["description"]
54
54
  }
55
55
 
56
- comments = issue_detail.get("comments", [])
57
- if comments:
58
- # Sort comments by created_at (created field)
59
- sorted_comments = sorted(comments, key=lambda c: convert_to_timezone(timezone, c["created"]))
60
- # Create list of {content, created_by} where created_by is just the name
61
- processed_comments = [
62
- {
63
- "content": c["content"],
64
- "created_by": c["createdUser"]["name"] if c.get("createdUser") else None
65
- }
66
- for c in sorted_comments if c.get("content")
67
- ]
68
- # Filter out None created_by if any (though should not happen)
69
- processed_comments = [c for c in processed_comments if c["created_by"]]
70
- processed_issue["comments"] = processed_comments
56
+ if include_comments:
57
+ comments = issue_detail.get("comments", [])
58
+ if comments:
59
+ # Sort comments by created_at (created field)
60
+ sorted_comments = sorted(comments, key=lambda c: convert_to_timezone(timezone, c["created"]))
61
+ # Create list of {content, created_by} where created_by is just the name
62
+ processed_comments = [
63
+ {
64
+ "content": c["content"],
65
+ "created_by": c["createdUser"]["name"] if c.get("createdUser") else None
66
+ }
67
+ for c in sorted_comments if c.get("content")
68
+ ]
69
+ # Filter out None created_by if any (though should not happen)
70
+ processed_comments = [c for c in processed_comments if c["created_by"]]
71
+ processed_issue["comments"] = processed_comments
71
72
 
72
73
  return processed_issue
73
74
 
@@ -77,6 +78,7 @@ async def get_issue_detail_handler(
77
78
  api_key: str,
78
79
  issue_key: str,
79
80
  timezone: str,
81
+ include_comments: bool = True,
80
82
  ):
81
83
  issue_comments_url = f"{backlog_domain}api/v2/issues/{issue_key}/comments"
82
84
  issue_detail_url = f"{backlog_domain}api/v2/issues/{issue_key}"
@@ -85,31 +87,38 @@ async def get_issue_detail_handler(
85
87
  async with httpx.AsyncClient() as client:
86
88
  try:
87
89
  issue_detail_response = client.get(issue_detail_url, params=params)
88
- comments_response = client.get(issue_comments_url, params=params)
89
-
90
- results = await asyncio.gather(issue_detail_response, comments_response)
91
-
92
- issue_detail = results[0].json()
93
- issue_comment = results[1].json()
90
+
91
+ if include_comments:
92
+ comments_response = client.get(issue_comments_url, params=params)
93
+ results = await asyncio.gather(issue_detail_response, comments_response)
94
+ issue_detail_result = results[0]
95
+ comments_result = results[1]
96
+ issue_detail = issue_detail_result.json()
97
+ issue_comment = comments_result.json()
98
+ else:
99
+ issue_detail_result = await issue_detail_response
100
+ issue_detail = issue_detail_result.json()
101
+ issue_comment = []
94
102
 
95
- if not results[0].is_success:
103
+ if not issue_detail_result.is_success:
96
104
  error_code = issue_detail["errors"][0]["code"]
97
105
  return {
98
106
  "error_msg": BacklogApiError.get_description_by_code(error_code),
99
107
  }
100
108
 
101
- if not results[1].is_success:
109
+ if include_comments and not comments_result.is_success:
102
110
  error_code = issue_comment["errors"][0]["code"]
103
111
  return {
104
112
  "error_msg": BacklogApiError.get_description_by_code(error_code),
105
113
  }
106
114
 
107
- comments_in_time_range = []
108
- for comment in issue_comment:
109
- comments_in_time_range.append(comment)
115
+ if include_comments:
116
+ comments_in_time_range = []
117
+ for comment in issue_comment:
118
+ comments_in_time_range.append(comment)
119
+ issue_detail.update({"comments": comments_in_time_range})
110
120
 
111
- issue_detail.update({"comments": comments_in_time_range})
112
- processed_detail = process_issue_detail(issue_detail, timezone, issue_key)
121
+ processed_detail = process_issue_detail(issue_detail, timezone, issue_key, include_comments)
113
122
  return processed_detail
114
123
 
115
124
  except Exception as e:
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "backlog-mcp"
3
- version = "1.0.3"
3
+ version = "1.0.4"
4
4
  description = "A Model Context Protocol (MCP) server for Backlog project management integration"
5
5
  authors = [{name = "BaoNguyen", email = "baonguyen@teqnological.asia"}]
6
6
  readme = "README.md"
@@ -0,0 +1,702 @@
1
+ #!/bin/bash
2
+
3
+ set -e
4
+
5
+ # Colors
6
+ RED='\033[0;31m'
7
+ GREEN='\033[0;32m'
8
+ YELLOW='\033[1;33m'
9
+ BLUE='\033[0;34m'
10
+ CYAN='\033[0;36m'
11
+ MAGENTA='\033[0;35m'
12
+ WHITE='\033[1;37m'
13
+ BOLD_CYAN='\033[1;36m'
14
+ BOLD_YELLOW='\033[1;33m'
15
+ BOLD_ORANGE='\033[1;38;5;166m'
16
+ NC='\033[0m' # No Color
17
+
18
+ clear
19
+ echo ""
20
+ echo -e "${BOLD_ORANGE}"
21
+ cat << "EOF"
22
+ _____ _____ ___ _ ____ ___ _
23
+ |_ _| ____/ _ \ / \ / ___|_ _| / \
24
+ | | | __| | | | / _ \ \___ \| | / _ \
25
+ | | | |__| |_| | / ___ \ ___) | | / ___ \
26
+ |_| |_____\__\_\ /_/ \_\____/___/_/ \_\
27
+
28
+ Backlog MCP
29
+ Installer v1.0
30
+ EOF
31
+ echo -e "${NC}"
32
+
33
+ # ============================================
34
+ # SYSTEM REQUIREMENTS CHECK
35
+ # ============================================
36
+ echo -e "${BOLD_CYAN}🔍 Checking system requirements...${NC}"
37
+ echo ""
38
+
39
+ # Check Python 3
40
+ echo -e "${WHITE} Checking Python 3...${NC}"
41
+ if command -v python3 &> /dev/null; then
42
+ PYTHON_VERSION=$(python3 --version 2>&1 | awk '{print $2}')
43
+ echo -e "${GREEN} ✓ Python 3 found: ${PYTHON_VERSION}${NC}"
44
+ else
45
+ echo -e "${RED} ✗ Python 3 not found${NC}"
46
+ echo -e "${YELLOW} Please install Python 3 first: https://www.python.org/downloads/${NC}"
47
+ exit 1
48
+ fi
49
+
50
+ # Check UV
51
+ echo -e "${WHITE} Checking UV (Python package installer)...${NC}"
52
+ if command -v uv &> /dev/null; then
53
+ UV_VERSION=$(uv --version 2>&1 | awk '{print $2}')
54
+ echo -e "${GREEN} ✓ UV found: ${UV_VERSION}${NC}"
55
+ elif command -v uvx &> /dev/null; then
56
+ UV_VERSION=$(uvx --version 2>&1 | head -n 1)
57
+ echo -e "${GREEN} ✓ UV found: ${UV_VERSION}${NC}"
58
+ else
59
+ echo -e "${YELLOW} ⚠️ UV not found on your system${NC}"
60
+ echo ""
61
+ echo -e "${WHITE} UV is required to run this MCP server.${NC}"
62
+ echo -e "${CYAN} UV is a fast Python package installer and resolver.${NC}"
63
+ echo -e " 🔗 More info: ${BLUE}https://github.com/astral-sh/uv${NC}"
64
+ echo ""
65
+ echo -e "${BOLD_YELLOW} Would you like to install UV now?${NC}"
66
+ echo ""
67
+ echo " 1) ✅ Yes - Install UV automatically"
68
+ echo " 2) ❌ No - Exit (you can install manually later)"
69
+ echo ""
70
+
71
+ while true; do
72
+ read -p " ➤ Enter your choice (1-2): " install_uv_choice
73
+
74
+ case $install_uv_choice in
75
+ 1)
76
+ echo ""
77
+ echo -e "${CYAN} 📥 Installing UV...${NC}"
78
+ echo ""
79
+
80
+ # Detect OS and install accordingly
81
+ if [[ "$OSTYPE" == "darwin"* ]]; then
82
+ # macOS
83
+ echo -e "${WHITE} Detected: macOS${NC}"
84
+
85
+ # Check if Homebrew is available
86
+ if command -v brew &> /dev/null; then
87
+ echo -e "${CYAN} Installing via Homebrew...${NC}"
88
+ brew install uv
89
+ else
90
+ echo -e "${CYAN} Installing via official installer...${NC}"
91
+ curl -LsSf https://astral.sh/uv/install.sh | sh
92
+ fi
93
+
94
+ elif [[ "$OSTYPE" == "linux-gnu"* ]]; then
95
+ # Linux
96
+ echo -e "${WHITE} Detected: Linux${NC}"
97
+ echo -e "${CYAN} Installing via official installer...${NC}"
98
+ curl -LsSf https://astral.sh/uv/install.sh | sh
99
+
100
+ elif [[ "$OSTYPE" == "msys" || "$OSTYPE" == "cygwin" ]]; then
101
+ # Windows (Git Bash/Cygwin)
102
+ echo -e "${WHITE} Detected: Windows${NC}"
103
+ echo -e "${CYAN} Installing via PowerShell...${NC}"
104
+ powershell -c "irm https://astral.sh/uv/install.ps1 | iex"
105
+
106
+ else
107
+ echo -e "${RED} ✗ Unsupported OS: $OSTYPE${NC}"
108
+ echo -e "${YELLOW} Please install UV manually: https://github.com/astral-sh/uv${NC}"
109
+ exit 1
110
+ fi
111
+
112
+ # Verify installation
113
+ echo ""
114
+ echo -e "${WHITE} Verifying UV installation...${NC}"
115
+
116
+ # Reload PATH
117
+ export PATH="$HOME/.cargo/bin:$HOME/.local/bin:$PATH"
118
+
119
+ if command -v uv &> /dev/null; then
120
+ UV_VERSION=$(uv --version 2>&1 | awk '{print $2}')
121
+ echo -e "${GREEN} ✓ UV successfully installed: ${UV_VERSION}${NC}"
122
+ elif command -v uvx &> /dev/null; then
123
+ UV_VERSION=$(uvx --version 2>&1 | head -n 1)
124
+ echo -e "${GREEN} ✓ UV successfully installed: ${UV_VERSION}${NC}"
125
+ else
126
+ echo -e "${YELLOW} ⚠️ UV installed but not found in PATH${NC}"
127
+ echo -e "${CYAN} Please restart your terminal and run this script again.${NC}"
128
+ echo ""
129
+ echo -e "${WHITE} Or add this to your PATH:${NC}"
130
+ echo -e " ${BLUE}export PATH=\"\$HOME/.cargo/bin:\$HOME/.local/bin:\$PATH\"${NC}"
131
+ echo ""
132
+ exit 1
133
+ fi
134
+
135
+ break
136
+ ;;
137
+ 2)
138
+ echo ""
139
+ echo -e "${YELLOW} Installation cancelled${NC}"
140
+ echo -e "${CYAN} 💡 You can install UV manually:${NC}"
141
+ echo -e " ${BLUE}https://github.com/astral-sh/uv${NC}"
142
+ echo ""
143
+ echo -e "${WHITE} Quick install commands:${NC}"
144
+ echo -e " ${CYAN}macOS/Linux:${NC} curl -LsSf https://astral.sh/uv/install.sh | sh"
145
+ echo -e " ${CYAN}Windows:${NC} powershell -c \"irm https://astral.sh/uv/install.ps1 | iex\""
146
+ echo ""
147
+ exit 0
148
+ ;;
149
+ *)
150
+ echo -e "${RED} ✗ Invalid choice. Please enter 1 or 2.${NC}"
151
+ ;;
152
+ esac
153
+ done
154
+ fi
155
+
156
+ echo ""
157
+ echo -e "${WHITE} ════════════════════════════════════════════════════════${NC}"
158
+ echo ""
159
+
160
+ # ============================================
161
+ # SET IDE CONSTANTS (VSCode only)
162
+ # ============================================
163
+ IDE_NAME="VS Code"
164
+ IDE_KEY="vscode"
165
+
166
+ # ============================================
167
+ # QUESTION 1: Which Agent?
168
+ # ============================================
169
+ echo -e "${BOLD_CYAN}🤖 Which AI coding agent are you using in VS Code?${NC}"
170
+ echo ""
171
+ echo " 1) 🔷 Cline"
172
+ echo " 2) 🤖 GitHub Copilot"
173
+ echo " 3) 🟣 Claude Code"
174
+ echo ""
175
+ read -p " ➤ Enter your choice (1-3): " agent_choice
176
+
177
+ # ============================================
178
+ # AGENT CONFIGURATION DEFINITIONS
179
+ # ============================================
180
+ # Each agent has different configuration structure
181
+ # Flags control which fields to include in the final JSON
182
+
183
+ case $agent_choice in
184
+ 1)
185
+ echo -e "${GREEN} ✓ Cline selected${NC}"
186
+ AGENT_NAME="Cline"
187
+ AGENT_KEY="cline"
188
+ CONFIG_FORMAT="standard"
189
+ SERVERS_KEY="mcpServers"
190
+ TIMEOUT_VALUE="60"
191
+ HAS_TYPE="true"
192
+ HAS_AUTO_APPROVE="true"
193
+ HAS_DISABLED="true"
194
+ HAS_TIMEOUT="true"
195
+ CONFIG_FILE="$HOME/Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json"
196
+ ;;
197
+ 2)
198
+ echo -e "${GREEN} ✓ GitHub Copilot selected${NC}"
199
+ AGENT_NAME="GitHub Copilot"
200
+ AGENT_KEY="copilot"
201
+ CONFIG_FORMAT="copilot"
202
+ SERVERS_KEY="servers"
203
+ TIMEOUT_VALUE="60"
204
+ HAS_TYPE="true"
205
+ HAS_AUTO_APPROVE="false"
206
+ HAS_DISABLED="false"
207
+ HAS_TIMEOUT="true"
208
+ CONFIG_FILE="$HOME/Library/Application Support/Code/User/mcp.json"
209
+ ;;
210
+ 3)
211
+ echo -e "${GREEN} ✓ Claude Code selected${NC}"
212
+ AGENT_NAME="Claude Code"
213
+ AGENT_KEY="claude-code"
214
+ CONFIG_FORMAT="claude-code"
215
+ SERVERS_KEY="mcpServers"
216
+ TIMEOUT_VALUE=""
217
+ HAS_TYPE="false"
218
+ HAS_AUTO_APPROVE="false"
219
+ HAS_DISABLED="false"
220
+ HAS_TIMEOUT="false"
221
+ CONFIG_FILE="$HOME/.claude.json"
222
+ ;;
223
+ *)
224
+ echo -e "${RED} ✗ Invalid choice${NC}"
225
+ exit 1
226
+ ;;
227
+ esac
228
+
229
+ echo ""
230
+ echo -e "${CYAN} 📁 Configuration file:${NC}"
231
+ echo -e " ${BLUE}$CONFIG_FILE${NC}"
232
+
233
+ echo ""
234
+ echo -e "${WHITE} ════════════════════════════════════════════════════════${NC}"
235
+ echo ""
236
+
237
+ # ============================================
238
+ # Check if MCP already exists
239
+ # ============================================
240
+ OVERWRITE_MODE="false"
241
+ MCP_EXISTS="false"
242
+
243
+ # Create a temporary Python script to check if MCP exists
244
+ CHECK_RESULT=$(python3 - "$CONFIG_FILE" "$SERVERS_KEY" "backlog-mcp" << 'PYTHON_CHECK_EOF'
245
+ import json
246
+ import sys
247
+ import os
248
+
249
+ def strip_json_comments(text):
250
+ """Remove comments from JSON-like text line by line"""
251
+ lines = text.split('\n')
252
+ cleaned_lines = []
253
+ in_multiline_comment = False
254
+
255
+ for line_idx, line in enumerate(lines):
256
+ original_line = line
257
+ # Handle multi-line comments
258
+ if '/*' in line:
259
+ in_multiline_comment = True
260
+ line = line[:line.index('/*')]
261
+
262
+ if in_multiline_comment:
263
+ if '*/' in line:
264
+ in_multiline_comment = False
265
+ line = line[line.index('*/') + 2:]
266
+ else:
267
+ continue
268
+
269
+ # Handle single-line comments
270
+ if '//' in line:
271
+ # Check if // is inside a string
272
+ in_string = False
273
+ quote_char = None
274
+ for i, char in enumerate(line):
275
+ if char in ['"', "'"] and (i == 0 or line[i-1] != '\\'):
276
+ if not in_string:
277
+ in_string = True
278
+ quote_char = char
279
+ elif char == quote_char:
280
+ in_string = False
281
+ quote_char = None
282
+
283
+ if not in_string and i < len(line) - 1 and line[i:i+2] == '//':
284
+ line = line[:i]
285
+ break
286
+
287
+ # Remove trailing commas before } or ]
288
+ line = line.rstrip()
289
+ if line.endswith(','):
290
+ temp_line = line[:-1].rstrip()
291
+ # Check if next significant character is } or ]
292
+ remaining = '\n'.join(lines[line_idx+1:])
293
+ stripped_remaining = remaining.lstrip()
294
+ if stripped_remaining and stripped_remaining[0] in '}]':
295
+ line = temp_line
296
+
297
+ if line.strip():
298
+ cleaned_lines.append(line)
299
+
300
+ return '\n'.join(cleaned_lines)
301
+
302
+ config_file = sys.argv[1]
303
+ servers_key = sys.argv[2]
304
+ mcp_name = sys.argv[3]
305
+
306
+ if not os.path.exists(config_file):
307
+ print("NOT_EXISTS")
308
+ sys.exit(0)
309
+
310
+ try:
311
+ with open(config_file, 'r', encoding='utf-8') as f:
312
+ content = f.read()
313
+
314
+ # Strip comments before parsing
315
+ clean_content = strip_json_comments(content)
316
+ config = json.loads(clean_content)
317
+
318
+ if servers_key in config and mcp_name in config[servers_key]:
319
+ print("EXISTS")
320
+ # Print current config details
321
+ mcp_config = config[servers_key][mcp_name]
322
+ print(f"COMMAND:{mcp_config.get('command', 'N/A')}")
323
+ print(f"TYPE:{mcp_config.get('type', 'N/A')}")
324
+ print(f"DISABLED:{mcp_config.get('disabled', False)}")
325
+
326
+ if 'env' in mcp_config:
327
+ print("ENV_VARS:")
328
+ for key, value in mcp_config['env'].items():
329
+ if any(sensitive in key.upper() for sensitive in ['TOKEN', 'PASSWORD', 'KEY', 'SECRET', 'API_KEY']):
330
+ print(f" {key}: *** (hidden)")
331
+ else:
332
+ print(f" {key}: {value}")
333
+ else:
334
+ print("NOT_EXISTS")
335
+ except json.JSONDecodeError as e:
336
+ print(f"ERROR:JSON Parse Error - {str(e)}")
337
+ except Exception as e:
338
+ print(f"ERROR:{str(e)}")
339
+ PYTHON_CHECK_EOF
340
+ )
341
+
342
+ # Parse the check result
343
+ if echo "$CHECK_RESULT" | grep -q "^EXISTS"; then
344
+ MCP_EXISTS="true"
345
+
346
+ echo -e "${YELLOW}⚠️ WARNING: backlog-mcp already exists!${NC}"
347
+ echo ""
348
+ echo -e "${WHITE} Configuration file:${NC}"
349
+ echo -e " ${CYAN}$CONFIG_FILE${NC}"
350
+ echo ""
351
+ echo -e "${WHITE} 📋 Current MCP configuration:${NC}"
352
+
353
+ # Display the configuration details
354
+ echo "$CHECK_RESULT" | grep -v "^EXISTS" | while IFS= read -r line; do
355
+ if [[ $line == COMMAND:* ]]; then
356
+ echo " Command: ${line#COMMAND:}"
357
+ elif [[ $line == TYPE:* ]]; then
358
+ echo " Type: ${line#TYPE:}"
359
+ elif [[ $line == DISABLED:* ]]; then
360
+ echo " Disabled: ${line#DISABLED:}"
361
+ elif [[ $line == "ENV_VARS:" ]]; then
362
+ echo " Environment variables:"
363
+ elif [[ $line == " "* ]]; then
364
+ echo " $line"
365
+ fi
366
+ done
367
+
368
+ echo ""
369
+ echo -e "${BOLD_YELLOW} ❓ Do you want to overwrite the existing configuration?${NC}"
370
+ echo ""
371
+ echo " 1) ✅ Yes - Overwrite with new configuration"
372
+ echo " 2) ❌ No - Cancel installation (keep existing)"
373
+ echo ""
374
+
375
+ while true; do
376
+ read -p " ➤ Enter your choice (1-2): " overwrite_choice
377
+
378
+ case $overwrite_choice in
379
+ 1)
380
+ echo -e "${GREEN} ✓ Will overwrite existing configuration${NC}"
381
+ OVERWRITE_MODE="true"
382
+ break
383
+ ;;
384
+ 2)
385
+ echo ""
386
+ echo -e "${YELLOW} ✓ Installation cancelled${NC}"
387
+ echo -e "${CYAN} 💡 No changes were made to your configuration.${NC}"
388
+ echo ""
389
+ exit 0
390
+ ;;
391
+ *)
392
+ echo -e "${RED} ✗ Invalid choice. Please enter 1 or 2.${NC}"
393
+ ;;
394
+ esac
395
+ done
396
+
397
+ echo ""
398
+ echo -e "${WHITE} ════════════════════════════════════════════════════════${NC}"
399
+ echo ""
400
+ elif echo "$CHECK_RESULT" | grep -q "^ERROR:"; then
401
+ ERROR_MSG=$(echo "$CHECK_RESULT" | grep "^ERROR:" | cut -d: -f2-)
402
+ echo ""
403
+ echo -e "${RED}✗ Error: Cannot read config file${NC}"
404
+ echo ""
405
+ echo -e "${WHITE} Configuration file:${NC}"
406
+ echo -e " ${CYAN}$CONFIG_FILE${NC}"
407
+ echo ""
408
+ echo -e "${WHITE} Error details:${NC}"
409
+ echo -e " ${RED}$ERROR_MSG${NC}"
410
+ echo ""
411
+ echo -e "${YELLOW} 💡 Please fix the JSON syntax manually:${NC}"
412
+ echo -e " 1. Open the file in your editor"
413
+ echo -e " 2. Remove any comments (// or /* */)"
414
+ echo -e " 3. Fix any JSON syntax errors"
415
+ echo -e " 4. Or delete the file and run this script again"
416
+ echo ""
417
+ exit 1
418
+ fi
419
+
420
+ # ============================================
421
+ # QUESTION 2: Environment Variables Configuration
422
+ # ============================================
423
+ echo -e "${BOLD_CYAN}🔑 Backlog MCP Configuration${NC}"
424
+ echo ""
425
+ echo -e "${WHITE} Please configure the following environment variables:${NC}"
426
+ echo ""
427
+
428
+ # BACKLOG_API_KEY (Variable - sensitive, will be prompted)
429
+ echo -e "${CYAN} ℹ️ BACKLOG_API_KEY${NC}"
430
+ echo -e "${YELLOW} ⚠️ Required: Please enter your BACKLOG_API_KEY${NC}"
431
+ while [ -z "$BACKLOG_API_KEY" ]; do
432
+ read -sp " 🔐 Enter BACKLOG_API_KEY: " BACKLOG_API_KEY
433
+ echo ""
434
+ if [ -z "$BACKLOG_API_KEY" ]; then
435
+ echo -e "${RED} ✗ BACKLOG_API_KEY cannot be empty. Please try again.${NC}"
436
+ else
437
+ echo -e "${GREEN} ✓ BACKLOG_API_KEY set (${#BACKLOG_API_KEY} characters)${NC}"
438
+ fi
439
+ done
440
+ echo ""
441
+
442
+ echo ""
443
+ echo -e "${WHITE} ════════════════════════════════════════════════════════${NC}"
444
+ echo ""
445
+
446
+ # ============================================
447
+ # Configuration Summary
448
+ # ============================================
449
+ echo -e "${BOLD_CYAN} 📊 Configuration Summary:${NC}"
450
+ echo -e " ${WHITE}Agent:${NC} $AGENT_NAME (VS Code)"
451
+ echo -e " ${WHITE}Config file:${NC} $CONFIG_FILE"
452
+ echo -e " ${WHITE}Format:${NC} $CONFIG_FORMAT ($SERVERS_KEY)"
453
+ if [ "$HAS_TIMEOUT" = "true" ] && [ -n "$TIMEOUT_VALUE" ]; then
454
+ echo -e " ${WHITE}Timeout:${NC} $TIMEOUT_VALUE"
455
+ fi
456
+ if [ "$OVERWRITE_MODE" = "true" ]; then
457
+ echo -e " ${WHITE}Mode:${NC} ${YELLOW}⚠️ OVERWRITE${NC}"
458
+ else
459
+ echo -e " ${WHITE}Mode:${NC} ${GREEN}✨ NEW INSTALLATION${NC}"
460
+ fi
461
+ echo ""
462
+ echo -e " ${WHITE}MCP Server:${NC}"
463
+ echo -e " ${CYAN}Name:${NC} backlog-mcp"
464
+ echo -e " ${CYAN}Command:${NC} uvx"
465
+ echo -e " ${CYAN}Environment:${NC} ${GREEN}✓ Configured${NC}"
466
+ echo ""
467
+ echo -e "${WHITE} ════════════════════════════════════════════════════════${NC}"
468
+ echo ""
469
+
470
+ # Create directory
471
+ mkdir -p "$(dirname "$CONFIG_FILE")"
472
+
473
+ # Validate and merge using Python
474
+ python3 - "$CONFIG_FILE" "$OVERWRITE_MODE" "$SERVERS_KEY" "$TIMEOUT_VALUE" "$HAS_TYPE" "$HAS_AUTO_APPROVE" "$HAS_DISABLED" "$HAS_TIMEOUT" "$CONFIG_FORMAT" "backlog-mcp" "uvx" "backlog-mcp@latest" "ENV_START" "BACKLOG_API_KEY=$BACKLOG_API_KEY" "BACKLOG_DOMAIN=teq-dev.backlog.com" << 'PYTHON_EOF'
475
+ import json
476
+ import sys
477
+ import os
478
+
479
+ def strip_json_comments(text):
480
+ """Remove comments from JSON-like text line by line"""
481
+ lines = text.split('\n')
482
+ cleaned_lines = []
483
+ in_multiline_comment = False
484
+
485
+ for line_idx, line in enumerate(lines):
486
+ original_line = line
487
+ # Handle multi-line comments
488
+ if '/*' in line:
489
+ in_multiline_comment = True
490
+ line = line[:line.index('/*')]
491
+
492
+ if in_multiline_comment:
493
+ if '*/' in line:
494
+ in_multiline_comment = False
495
+ line = line[line.index('*/') + 2:]
496
+ else:
497
+ continue
498
+
499
+ # Handle single-line comments
500
+ if '//' in line:
501
+ # Check if // is inside a string
502
+ in_string = False
503
+ quote_char = None
504
+ for i, char in enumerate(line):
505
+ if char in ['"', "'"] and (i == 0 or line[i-1] != '\\'):
506
+ if not in_string:
507
+ in_string = True
508
+ quote_char = char
509
+ elif char == quote_char:
510
+ in_string = False
511
+ quote_char = None
512
+
513
+ if not in_string and i < len(line) - 1 and line[i:i+2] == '//':
514
+ line = line[:i]
515
+ break
516
+
517
+ # Remove trailing commas before } or ]
518
+ line = line.rstrip()
519
+ if line.endswith(','):
520
+ temp_line = line[:-1].rstrip()
521
+ # Check if next significant character is } or ]
522
+ remaining = '\n'.join(lines[line_idx+1:])
523
+ stripped_remaining = remaining.lstrip()
524
+ if stripped_remaining and stripped_remaining[0] in '}]':
525
+ line = temp_line
526
+
527
+ if line.strip():
528
+ cleaned_lines.append(line)
529
+
530
+ return '\n'.join(cleaned_lines)
531
+
532
+ config_file = sys.argv[1]
533
+ overwrite_mode = sys.argv[2] == "true"
534
+ servers_key = sys.argv[3]
535
+ timeout_value = sys.argv[4]
536
+ has_type = sys.argv[5] == "true"
537
+ has_auto_approve = sys.argv[6] == "true"
538
+ has_disabled = sys.argv[7] == "true"
539
+ has_timeout = sys.argv[8] == "true"
540
+ config_format = sys.argv[9]
541
+ mcp_name = sys.argv[10]
542
+ command = sys.argv[11]
543
+
544
+ # Parse args (starting from index 12)
545
+ args = []
546
+ env_vars = {}
547
+ arg_index = 12
548
+
549
+ # Parse args until we hit the env vars marker
550
+ while arg_index < len(sys.argv):
551
+ arg = sys.argv[arg_index]
552
+ if arg == "ENV_START":
553
+ arg_index += 1
554
+ break
555
+ args.append(arg)
556
+ arg_index += 1
557
+
558
+ # Parse env vars (format: KEY=VALUE)
559
+ while arg_index < len(sys.argv):
560
+ env_pair = sys.argv[arg_index]
561
+ if '=' in env_pair:
562
+ key, value = env_pair.split('=', 1)
563
+ env_vars[key] = value
564
+ arg_index += 1
565
+
566
+ # ANSI colors
567
+ RED = '\033[0;31m'
568
+ GREEN = '\033[0;32m'
569
+ YELLOW = '\033[1;33m'
570
+ BLUE = '\033[0;34m'
571
+ CYAN = '\033[1;36m'
572
+ WHITE = '\033[1;37m'
573
+ NC = '\033[0m'
574
+
575
+ def print_color(color, message):
576
+ print(f"{color}{message}{NC}")
577
+
578
+ # Check if file exists
579
+ if os.path.exists(config_file):
580
+ try:
581
+ with open(config_file, 'r', encoding='utf-8') as f:
582
+ content = f.read()
583
+
584
+ # Strip comments before parsing
585
+ clean_content = strip_json_comments(content)
586
+ config = json.loads(clean_content)
587
+
588
+ # Validate structure
589
+ if not isinstance(config, dict):
590
+ print_color(RED, " ✗ Error: Config file is not a valid JSON object")
591
+ print(f" 📝 File: {config_file}")
592
+ print(f" 🔧 Expected: {{ \"{servers_key}\": {{...}} }}")
593
+ sys.exit(1)
594
+
595
+ if servers_key in config:
596
+ if not isinstance(config[servers_key], dict):
597
+ print_color(RED, f" ✗ Error: '{servers_key}' must be an object")
598
+ print(f" 📝 File: {config_file}")
599
+ sys.exit(1)
600
+ else:
601
+ config[servers_key] = {}
602
+
603
+ if overwrite_mode:
604
+ print_color(YELLOW, " ⚠️ Overwriting existing configuration...")
605
+ else:
606
+ print_color(CYAN, f" 📋 Found {len(config[servers_key])} existing MCP server(s)")
607
+
608
+ except json.JSONDecodeError as e:
609
+ print_color(RED, " ✗ Error: Invalid JSON in config file")
610
+ print(f" 📝 File: {config_file}")
611
+ print(f" 🔍 Error: {str(e)}")
612
+ print("")
613
+ print_color(YELLOW, " 💡 Please fix the JSON syntax manually:")
614
+ print(" 1. Open the file in your editor")
615
+ print(" 2. Remove any comments (// or /* */)")
616
+ print(" 3. Fix any JSON syntax errors")
617
+ print(" 4. Or delete the file and run this script again")
618
+ sys.exit(1)
619
+ except Exception as e:
620
+ print_color(RED, f" ✗ Error reading config: {str(e)}")
621
+ sys.exit(1)
622
+ else:
623
+ print_color(WHITE, " 📝 Config file not found, creating new one...")
624
+ config = {servers_key: {}}
625
+
626
+ # Prepare the MCP configuration (base config - always included)
627
+ mcp_config = {
628
+ "command": command,
629
+ "args": args,
630
+ "env": env_vars
631
+ }
632
+
633
+ # Add optional fields based on agent configuration flags
634
+ if has_type:
635
+ mcp_config["type"] = "stdio"
636
+
637
+ if has_auto_approve:
638
+ mcp_config["autoApprove"] = ["get_issue_details", "get_user_issue_list"]
639
+
640
+ if has_disabled:
641
+ mcp_config["disabled"] = False
642
+
643
+ if has_timeout and timeout_value:
644
+ # Convert timeout based on agent type:
645
+ # - Copilot: milliseconds (convert seconds to milliseconds)
646
+ # - Cline: seconds (keep as is)
647
+ # - Claude Code: not supported (has_timeout is false)
648
+ timeout_int = int(timeout_value)
649
+ if config_format == "copilot":
650
+ # Copilot uses milliseconds, convert from seconds
651
+ mcp_config["timeout"] = timeout_int * 1000
652
+ else:
653
+ # Cline uses seconds
654
+ mcp_config["timeout"] = timeout_int
655
+
656
+ # Add/Update MCP server
657
+ config[servers_key][mcp_name] = mcp_config
658
+
659
+ # Write back (clean JSON without comments)
660
+ try:
661
+ with open(config_file, 'w', encoding='utf-8') as f:
662
+ json.dump(config, f, indent=4, ensure_ascii=False)
663
+
664
+ if overwrite_mode:
665
+ print_color(GREEN, f" ✓ Successfully overwritten {mcp_name}")
666
+ else:
667
+ print_color(GREEN, f" ✓ Successfully added {mcp_name}")
668
+
669
+ print(f" 📝 Config: {config_file}")
670
+ print_color(CYAN, f" 🔢 Total MCP servers: {len(config[servers_key])}")
671
+
672
+ # List all servers
673
+ print_color(WHITE, f"\n 📋 All MCP servers (format: {config_format}):")
674
+ for server_name in config[servers_key].keys():
675
+ server_config = config[servers_key][server_name]
676
+ status = "disabled" if server_config.get('disabled', False) else "enabled"
677
+ emoji = "🔴" if server_config.get('disabled', False) else "🟢"
678
+ has_env = "🔐" if server_config.get('env') else ""
679
+ marker = "⚡" if server_name == mcp_name else " "
680
+ print(f" {marker} {emoji} {server_name} ({status}) {has_env}")
681
+
682
+ print_color(GREEN, "\n ✅ Installation complete!")
683
+ print_color(YELLOW, " 🔄 Please restart VS Code and your agent to apply changes")
684
+
685
+ if config_format != "claude-code":
686
+ print_color(CYAN, "\n 💡 Note: JSON comments were removed from the file")
687
+ print_color(CYAN, " (This is normal - the file will still work perfectly)")
688
+
689
+ except Exception as e:
690
+ print_color(RED, f" ✗ Error writing config: {str(e)}")
691
+ sys.exit(1)
692
+ PYTHON_EOF
693
+
694
+ if [ $? -eq 0 ]; then
695
+ echo ""
696
+ echo -e "${WHITE}"
697
+ cat << "EOF"
698
+ ✨ Installation Success! ✨
699
+ 🎉 All Done! 🎉
700
+ EOF
701
+ echo -e "${NC}"
702
+ fi
@@ -34,7 +34,7 @@ wheels = [
34
34
 
35
35
  [[package]]
36
36
  name = "backlog-mcp"
37
- version = "1.0.2"
37
+ version = "1.0.3"
38
38
  source = { editable = "." }
39
39
  dependencies = [
40
40
  { name = "httpx" },
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes