Skip to content

Commit 8ecc3ef

Browse files
committed
fix(handlers): handle 204 No Content responses and FormData uploads
FIXES: - Handle 204 No Content responses that caused JSON parse errors: - delete_variable: returns {"deleted": true} - delete_milestone: returns {"deleted": true} - publish_draft_note: returns {"published": true} - bulk_publish_draft_notes: returns {"published": true} - Fix FormData Content-Type in enhancedFetch: - Skip Content-Type header for FormData (let fetch set multipart boundary) - Fixes upload_markdown returning 400 Bad Request - Fix upload_markdown file handling: - Use File object instead of Blob for proper multipart handling - Node.js requires File (not Blob with filename) for uploads
1 parent 50e7810 commit 8ecc3ef

File tree

15 files changed

+170
-59
lines changed

15 files changed

+170
-59
lines changed

.env.example

Lines changed: 135 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
# REQUIRED CONFIGURATION
66
# ============================================================================
77

8-
# GitLab Personal Access Token (REQUIRED)
8+
# GitLab Personal Access Token (REQUIRED when OAuth is disabled)
99
# Create at: GitLab → Settings → Access Tokens
1010
# Required scopes: api, read_user, read_repository
1111
# For full functionality add: write_repository, read_api, write_api
@@ -22,50 +22,117 @@ GITLAB_API_URL=https://gitlab.com
2222
# TRANSPORT CONFIGURATION
2323
# ============================================================================
2424

25-
# HTTP Streamable mode (recommended for web clients)
26-
# Set to true for HTTP streaming transport
27-
STREAMABLE_HTTP=true
25+
# Server host binding
26+
# Use 0.0.0.0 for external access, 127.0.0.1 for local only
27+
HOST=0.0.0.0
2828

29-
# Server-Sent Events mode
30-
# Set to true for SSE transport (note: only one transport should be true)
31-
SSE=false
29+
# Server port for HTTP/SSE modes
30+
# Default: 3002
31+
PORT=3002
3232

33-
# Server host for HTTP/SSE modes
34-
HOST=127.0.0.1
33+
# ============================================================================
34+
# TLS/HTTPS CONFIGURATION (Direct TLS Termination)
35+
# ============================================================================
36+
# For reverse proxy TLS (nginx/envoy/caddy), see SSL.md
3537

36-
# Server port for HTTP/SSE modes
37-
PORT=3000
38+
# Path to PEM certificate file
39+
# SSL_CERT_PATH=/path/to/server.crt
40+
41+
# Path to PEM private key file
42+
# SSL_KEY_PATH=/path/to/server.key
43+
44+
# Path to CA certificate chain (for client cert validation)
45+
# SSL_CA_PATH=/path/to/ca.crt
46+
47+
# Passphrase for encrypted private keys
48+
# SSL_PASSPHRASE=your_passphrase
49+
50+
# ============================================================================
51+
# REVERSE PROXY CONFIGURATION
52+
# ============================================================================
53+
# When behind nginx, envoy, caddy, or traefik
54+
55+
# Trust proxy setting for X-Forwarded-* headers
56+
# Values: true, false, loopback, linklocal, uniquelocal, or number of hops
57+
# TRUST_PROXY=true
58+
59+
# ============================================================================
60+
# OAUTH CONFIGURATION (Per-User Authentication)
61+
# ============================================================================
62+
# Enable OAuth mode for per-user GitLab authentication
63+
# When enabled, each user authenticates with their own GitLab credentials
64+
65+
# Enable OAuth mode (disables GITLAB_TOKEN requirement)
66+
OAUTH_ENABLED=false
67+
68+
# Secret for signing MCP JWT tokens (minimum 32 characters)
69+
# Generate with: openssl rand -base64 32
70+
# OAUTH_SESSION_SECRET=your_32_char_minimum_secret_here
71+
72+
# GitLab OAuth Application Client ID
73+
# Create at: GitLab → Admin → Applications (or User → Settings → Applications)
74+
# GITLAB_OAUTH_CLIENT_ID=your_client_id
75+
76+
# GitLab OAuth Application Client Secret (optional, for confidential apps)
77+
# GITLAB_OAUTH_CLIENT_SECRET=your_client_secret
78+
79+
# OAuth scopes to request from GitLab (default: api,read_user)
80+
# GITLAB_OAUTH_SCOPES=api,read_user
81+
82+
# MCP access token TTL in seconds (default: 3600 = 1 hour)
83+
# OAUTH_TOKEN_TTL=3600
84+
85+
# MCP refresh token TTL in seconds (default: 604800 = 7 days)
86+
# OAUTH_REFRESH_TOKEN_TTL=604800
87+
88+
# Device flow polling interval in seconds (default: 5)
89+
# OAUTH_DEVICE_POLL_INTERVAL=5
90+
91+
# Device flow timeout in seconds (default: 300 = 5 minutes)
92+
# OAUTH_DEVICE_TIMEOUT=300
3893

3994
# ============================================================================
4095
# OPTIONAL GITLAB TARGETING
4196
# ============================================================================
4297

4398
# Default project for operations that don't specify a target
4499
# Format: "username/project" or "group/project" or project ID
45-
# Leave empty if not using default targeting
46100
GITLAB_PROJECT_ID=
47101

102+
# Comma-separated list of allowed project IDs (empty = all allowed)
103+
# Use for restricting access to specific projects
104+
GITLAB_ALLOWED_PROJECT_IDS=
105+
48106
# Default group for group-level operations
49107
# Format: "group-name" or "parent-group/sub-group"
50-
# Leave empty if not using default group targeting
51108
GITLAB_GROUP_PATH=
52109

53110
# ============================================================================
54-
# FEATURE FLAGS (GitLab Tier-specific)
111+
# FEATURE FLAGS
55112
# ============================================================================
56113

57-
# Enable Work Items (GitLab Premium/Ultimate)
58-
# Set to false for GitLab Free tier
114+
# Enable Work Items (Epics, Issues via GraphQL API)
59115
USE_WORKITEMS=true
60116

61-
# Enable Milestones (GitLab Premium/Ultimate for group milestones)
62-
# Project milestones available in all tiers
117+
# Enable Labels functionality
118+
USE_LABELS=true
119+
120+
# Enable Merge Requests functionality
121+
USE_MRS=true
122+
123+
# Enable Files/Repository functionality
124+
USE_FILES=true
125+
126+
# Enable Milestones functionality
63127
USE_MILESTONE=true
64128

65-
# Enable CI/CD Pipelines (available in all tiers)
129+
# Enable CI/CD Pipelines functionality
66130
USE_PIPELINE=true
67131

68-
# Enable Wiki functionality (available in all tiers)
132+
# Enable CI/CD Variables functionality
133+
USE_VARIABLES=true
134+
135+
# Enable Wiki functionality
69136
USE_GITLAB_WIKI=true
70137

71138
# ============================================================================
@@ -78,28 +145,30 @@ GITLAB_READ_ONLY_MODE=false
78145

79146
# Regex pattern to deny specific tools
80147
# Example: "delete.*|create.*" to block delete and create operations
81-
# Leave empty to allow all tools
82148
GITLAB_DENIED_TOOLS_REGEX=
83149

84150
# ============================================================================
85-
# LOGGING AND DEBUGGING
151+
# TLS VERIFICATION
86152
# ============================================================================
87153

88-
# Log level: error, warn, info, debug, trace
89-
LOG_LEVEL=info
154+
# Skip TLS certificate verification (INSECURE - use only for testing)
155+
SKIP_TLS_VERIFY=false
90156

91-
# Node.js environment
92-
NODE_ENV=development
157+
# Path to custom CA certificate for GitLab server
158+
# GITLAB_CA_CERT_PATH=/path/to/ca-bundle.crt
159+
160+
# Node.js TLS rejection (0 = disable verification, INSECURE)
161+
# NODE_TLS_REJECT_UNAUTHORIZED=1
93162

94163
# ============================================================================
95164
# NETWORK AND PROXY SETTINGS
96165
# ============================================================================
97166

98167
# HTTP proxy for outbound requests
99-
HTTP_PROXY=
168+
# HTTP_PROXY=http://proxy.company.com:8080
100169

101170
# HTTPS proxy for outbound requests
102-
HTTPS_PROXY=
171+
# HTTPS_PROXY=http://proxy.company.com:8080
103172

104173
# Comma-separated list of hosts to bypass proxy
105174
NO_PROXY=localhost,127.0.0.1
@@ -108,26 +177,54 @@ NO_PROXY=localhost,127.0.0.1
108177
# PERFORMANCE TUNING
109178
# ============================================================================
110179

180+
# GitLab API timeout in milliseconds (default: 20000 = 20 seconds)
181+
GITLAB_API_TIMEOUT_MS=20000
182+
111183
# Node.js memory limit and options
112184
NODE_OPTIONS=--max-old-space-size=512
113185

114-
# Health check endpoint
115-
HEALTH_CHECK_ENABLED=true
186+
# ============================================================================
187+
# LOGGING AND DEBUGGING
188+
# ============================================================================
189+
190+
# Log level: error, warn, info, debug, trace
191+
LOG_LEVEL=info
192+
193+
# Node.js environment
194+
NODE_ENV=development
195+
196+
# ============================================================================
197+
# ADVANCED/EXPERIMENTAL
198+
# ============================================================================
199+
200+
# Cookie-based authentication (alternative to token)
201+
# Path to Netscape cookie file for GitLab authentication
202+
# GITLAB_AUTH_COOKIE_PATH=/path/to/cookies.txt
203+
204+
# Legacy GitLab instance mode (for older GitLab versions)
205+
# GITLAB_IS_OLD=false
116206

117207
# ============================================================================
118208
# USAGE NOTES
119209
# ============================================================================
120210

121-
# Transport Mode Priority:
122-
# 1. STREAMABLE_HTTP=true (highest priority, recommended)
123-
# 2. SSE=true (if STREAMABLE_HTTP!=true)
124-
# 3. Stdio mode (if both STREAMABLE_HTTP and SSE are false)
211+
# Transport Mode:
212+
# - Set PORT to enable HTTP server with dual transport (SSE + StreamableHTTP)
213+
# - Unset PORT for stdio mode (direct MCP communication)
214+
215+
# Authentication Modes:
216+
# 1. Static Token (default): Set GITLAB_TOKEN
217+
# 2. OAuth Mode: Set OAUTH_ENABLED=true and configure OAuth settings
218+
# - Each user authenticates with their own GitLab credentials
219+
# - Supports GitLab Device Flow for MCP clients
125220

126-
# GitLab Tier Compatibility:
127-
# - GitLab Free: Set USE_WORKITEMS=false
128-
# - GitLab Premium/Ultimate: All features available
221+
# TLS/HTTPS Options:
222+
# 1. Direct TLS: Set SSL_CERT_PATH and SSL_KEY_PATH
223+
# 2. Reverse Proxy: Use nginx/envoy/caddy for TLS termination
224+
# See SSL.md for detailed configuration examples
129225

130226
# Security Best Practices:
131227
# - Use read-only mode for security-sensitive environments
132228
# - Limit tool access with GITLAB_DENIED_TOOLS_REGEX
133-
# - Use environment-specific API URLs and tokens
229+
# - Use environment-specific API URLs and tokens
230+
# - Enable TLS for production deployments

src/entities/files/registry.ts

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -194,21 +194,12 @@ export const filesToolRegistry: ToolRegistry = new Map<string, EnhancedToolDefin
194194
// Create FormData for file upload
195195
const formData = new FormData();
196196

197-
// Convert base64 file content to blob if needed
198-
let fileBlob: Blob;
199-
if (typeof file === "string") {
200-
// Assume it's base64 encoded
201-
const binaryString = Buffer.from(file, "base64").toString("binary");
202-
const bytes = new Uint8Array(binaryString.length);
203-
for (let i = 0; i < binaryString.length; i++) {
204-
bytes[i] = binaryString.charCodeAt(i);
205-
}
206-
fileBlob = new Blob([bytes]);
207-
} else {
208-
fileBlob = file as Blob;
209-
}
197+
// Convert base64 file content to File object
198+
// Node.js requires File (not Blob with filename) for proper multipart handling
199+
const buffer = Buffer.from(file, "base64");
200+
const fileObj = new File([buffer], filename, { type: "application/octet-stream" });
210201

211-
formData.append("file", fileBlob, filename);
202+
formData.append("file", fileObj);
212203

213204
const apiUrl = `${process.env.GITLAB_API_URL}/api/v4/projects/${normalizeProjectId(project_id)}/uploads`;
214205
const response = await enhancedFetch(apiUrl, {

src/entities/milestones/registry.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -316,7 +316,8 @@ export const milestonesToolRegistry: ToolRegistry = new Map<string, EnhancedTool
316316
throw new Error(`GitLab API error: ${response.status} ${response.statusText}`);
317317
}
318318

319-
const result = await response.json();
319+
// DELETE returns 204 No Content on success
320+
const result = response.status === 204 ? { deleted: true } : await response.json();
320321
return cleanGidsFromObject(result);
321322
},
322323
},

src/entities/mrs/registry.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -492,7 +492,8 @@ export const mrsToolRegistry: ToolRegistry = new Map<string, EnhancedToolDefinit
492492
throw new Error(`GitLab API error: ${response.status} ${response.statusText}`);
493493
}
494494

495-
const result = await response.json();
495+
// PUT publish returns 204 No Content on success
496+
const result = response.status === 204 ? { published: true } : await response.json();
496497
return cleanGidsFromObject(result);
497498
},
498499
},
@@ -519,7 +520,8 @@ export const mrsToolRegistry: ToolRegistry = new Map<string, EnhancedToolDefinit
519520
throw new Error(`GitLab API error: ${response.status} ${response.statusText}`);
520521
}
521522

522-
const result = await response.json();
523+
// POST bulk_publish returns 204 No Content on success
524+
const result = response.status === 204 ? { published: true } : await response.json();
523525
return cleanGidsFromObject(result);
524526
},
525527
},

src/entities/variables/registry.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -334,7 +334,8 @@ export const variablesToolRegistry: ToolRegistry = new Map<string, EnhancedToolD
334334
throw new Error(errorMessage);
335335
}
336336

337-
const result = await response.json();
337+
// DELETE returns 204 No Content on success
338+
const result = response.status === 204 ? { deleted: true } : await response.json();
338339
return cleanGidsFromObject(result);
339340
},
340341
},

src/utils/fetch.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,13 @@ export async function enhancedFetch(url: string, options: RequestInit = {}): Pro
185185
const dispatcher = getDispatcher();
186186
const cookieHeader = loadCookieHeader();
187187

188-
const headers: Record<string, string> = { ...DEFAULT_HEADERS };
188+
// For FormData, don't set Content-Type - let fetch set it with proper boundary
189+
const isFormData = options.body instanceof FormData;
190+
const baseHeaders = isFormData
191+
? { "User-Agent": DEFAULT_HEADERS["User-Agent"], Accept: DEFAULT_HEADERS.Accept }
192+
: { ...DEFAULT_HEADERS };
193+
194+
const headers: Record<string, string> = { ...baseHeaders };
189195

190196
const authHeader = getAuthorizationHeader();
191197
if (authHeader) {
File renamed without changes.

0 commit comments

Comments
 (0)