Skip to content

Commit 865dee7

Browse files
authored
feat: add support for configuring custom HTTP headers (sooperset#570)
Add support for configuring custom HTTP headers for both Jira and Confluence API requests via environment variables. This enables users in corporate environments to add required authentication, routing, or security headers. Changes: - Add get_custom_headers() utility function for parsing comma-separated key=value pairs - Add custom_headers field to JiraConfig and ConfluenceConfig classes - Apply custom headers to underlying session objects during client initialization - Support service-specific headers via JIRA_CUSTOM_HEADERS and CONFLUENCE_CUSTOM_HEADERS - Header values masked in debug logs to protect sensitive information - Graceful handling of malformed headers (skip invalid, continue processing valid) - Comprehensive test coverage with 470+ lines of tests - Documentation with examples, debugging guide, and security considerations Usage: JIRA_CUSTOM_HEADERS=X-Corp-Auth=token123,X-Dept=engineering CONFLUENCE_CUSTOM_HEADERS=X-ALB-Token=secret,X-Service=mcp-atlassian This enables MCP server usage in corporate environments requiring additional HTTP headers for security, authentication, or routing. Reported-by: Sajjad Pervaiz Github-Issue: sooperset#436
1 parent 67393f5 commit 865dee7

File tree

10 files changed

+650
-2
lines changed

10 files changed

+650
-2
lines changed

.env.example

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,3 +135,10 @@ CONFLUENCE_URL=https://your-company.atlassian.net/wiki
135135
#CONFLUENCE_HTTPS_PROXY=https://confluence-proxy.example.com:8443
136136
#CONFLUENCE_SOCKS_PROXY=socks5://confluence-proxy.example.com:1080
137137
#CONFLUENCE_NO_PROXY=localhost,127.0.0.1,.internal.confluence.com
138+
139+
# --- Custom HTTP Headers (Advanced) ---
140+
# Jira-specific custom headers.
141+
#JIRA_CUSTOM_HEADERS=X-Jira-Service=mcp-integration,X-Custom-Auth=jira-token,X-Forwarded-User=service-account
142+
143+
# Confluence-specific custom headers.
144+
#CONFLUENCE_CUSTOM_HEADERS=X-Confluence-Service=mcp-integration,X-Custom-Auth=confluence-token,X-ALB-Token=secret-token

README.md

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -373,6 +373,56 @@ Add the relevant proxy variables to the `args` (using `-e`) and `env` sections o
373373
Credentials in proxy URLs are masked in logs. If you set `NO_PROXY`, it will be respected for requests to matching hosts.
374374

375375
</details>
376+
<details>
377+
<summary>Custom HTTP Headers Configuration</summary>
378+
379+
MCP Atlassian supports adding custom HTTP headers to all API requests. This feature is particularly useful in corporate environments where additional headers are required for security, authentication, or routing purposes.
380+
381+
Custom headers are configured using environment variables with comma-separated key=value pairs:
382+
383+
```json
384+
{
385+
"mcpServers": {
386+
"mcp-atlassian": {
387+
"command": "docker",
388+
"args": [
389+
"run",
390+
"-i",
391+
"--rm",
392+
"-e", "CONFLUENCE_URL",
393+
"-e", "CONFLUENCE_USERNAME",
394+
"-e", "CONFLUENCE_API_TOKEN",
395+
"-e", "CONFLUENCE_CUSTOM_HEADERS",
396+
"-e", "JIRA_URL",
397+
"-e", "JIRA_USERNAME",
398+
"-e", "JIRA_API_TOKEN",
399+
"-e", "JIRA_CUSTOM_HEADERS",
400+
"ghcr.io/sooperset/mcp-atlassian:latest"
401+
],
402+
"env": {
403+
"CONFLUENCE_URL": "https://your-company.atlassian.net/wiki",
404+
"CONFLUENCE_USERNAME": "[email protected]",
405+
"CONFLUENCE_API_TOKEN": "your_confluence_api_token",
406+
"CONFLUENCE_CUSTOM_HEADERS": "X-Confluence-Service=mcp-integration,X-Custom-Auth=confluence-token,X-ALB-Token=secret-token",
407+
"JIRA_URL": "https://your-company.atlassian.net",
408+
"JIRA_USERNAME": "[email protected]",
409+
"JIRA_API_TOKEN": "your_jira_api_token",
410+
"JIRA_CUSTOM_HEADERS": "X-Forwarded-User=service-account,X-Company-Service=mcp-atlassian,X-Jira-Client=mcp-integration"
411+
}
412+
}
413+
}
414+
}
415+
```
416+
417+
**Security Considerations:**
418+
419+
- Custom header values are masked in debug logs to protect sensitive information
420+
- Ensure custom headers don't conflict with standard HTTP or Atlassian API headers
421+
- Avoid including sensitive authentication tokens in custom headers if already using basic auth or OAuth
422+
- Headers are sent with every API request - verify they don't interfere with API functionality
423+
424+
</details>
425+
376426

377427
<details>
378428
<summary>Multi-Cloud OAuth Support</summary>
@@ -755,6 +805,43 @@ The server provides two ways to control tool access:
755805
- For older Confluence servers: Some older versions require basic authentication with `CONFLUENCE_USERNAME` and `CONFLUENCE_API_TOKEN` (where token is your password)
756806
- **SSL Certificate Issues**: If using Server/Data Center and encounter SSL errors, set `CONFLUENCE_SSL_VERIFY=false` or `JIRA_SSL_VERIFY=false`
757807
- **Permission Errors**: Ensure your Atlassian account has sufficient permissions to access the spaces/projects
808+
- **Custom Headers Issues**: See the ["Debugging Custom Headers"](#debugging-custom-headers) section below to analyze and resolve issues with custom headers
809+
810+
### Debugging Custom Headers
811+
812+
To verify custom headers are being applied correctly:
813+
814+
1. **Enable Debug Logging**: Set `MCP_VERY_VERBOSE=true` to see detailed request logs
815+
```bash
816+
# In your .env file or environment
817+
MCP_VERY_VERBOSE=true
818+
MCP_LOGGING_STDOUT=true
819+
```
820+
821+
2. **Check Header Parsing**: Custom headers appear in logs with masked values for security:
822+
```
823+
DEBUG Custom headers applied: {'X-Forwarded-User': '***', 'X-ALB-Token': '***'}
824+
```
825+
826+
3. **Verify Service-Specific Headers**: Check logs to confirm the right headers are being used:
827+
```
828+
DEBUG Jira request headers: service-specific headers applied
829+
DEBUG Confluence request headers: service-specific headers applied
830+
```
831+
832+
4. **Test Header Format**: Ensure your header string format is correct:
833+
```bash
834+
# Correct format
835+
JIRA_CUSTOM_HEADERS=X-Custom=value1,X-Other=value2
836+
CONFLUENCE_CUSTOM_HEADERS=X-Custom=value1,X-Other=value2
837+
838+
# Incorrect formats (will be ignored)
839+
JIRA_CUSTOM_HEADERS="X-Custom=value1,X-Other=value2" # Extra quotes
840+
JIRA_CUSTOM_HEADERS=X-Custom: value1,X-Other: value2 # Colon instead of equals
841+
JIRA_CUSTOM_HEADERS=X-Custom = value1 # Spaces around equals
842+
```
843+
844+
**Security Note**: Header values containing sensitive information (tokens, passwords) are automatically masked in logs to prevent accidental exposure.
758845
759846
### Debugging Tools
760847

src/mcp_atlassian/confluence/client.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,10 @@ def __init__(self, config: ConfluenceConfig | None = None) -> None:
114114
os.environ["NO_PROXY"] = self.config.no_proxy
115115
log_config_param(logger, "Confluence", "NO_PROXY", self.config.no_proxy)
116116

117+
# Apply custom headers if configured
118+
if self.config.custom_headers:
119+
self._apply_custom_headers()
120+
117121
# Import here to avoid circular imports
118122
from ..preprocessing.confluence import ConfluencePreprocessor
119123

@@ -158,6 +162,18 @@ def _validate_authentication(self) -> None:
158162
)
159163
raise MCPAtlassianAuthenticationError(error_msg) from e
160164

165+
def _apply_custom_headers(self) -> None:
166+
"""Apply custom headers to the Confluence session."""
167+
if not self.config.custom_headers:
168+
return
169+
170+
logger.debug(
171+
f"Applying {len(self.config.custom_headers)} custom headers to Confluence session"
172+
)
173+
for header_name, header_value in self.config.custom_headers.items():
174+
self.confluence._session.headers[header_name] = header_value
175+
logger.debug(f"Applied custom header: {header_name}")
176+
161177
def _process_html_content(
162178
self, html_content: str, space_key: str
163179
) -> tuple[str, str]:

src/mcp_atlassian/confluence/config.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from dataclasses import dataclass
66
from typing import Literal
77

8-
from ..utils.env import is_env_ssl_verify
8+
from ..utils.env import get_custom_headers, is_env_ssl_verify
99
from ..utils.oauth import (
1010
BYOAccessTokenOAuthConfig,
1111
OAuthConfig,
@@ -35,6 +35,7 @@ class ConfluenceConfig:
3535
https_proxy: str | None = None # HTTPS proxy URL
3636
no_proxy: str | None = None # Comma-separated list of hosts to bypass proxy
3737
socks_proxy: str | None = None # SOCKS proxy URL (optional)
38+
custom_headers: dict[str, str] | None = None # Custom HTTP headers
3839

3940
@property
4041
def is_cloud(self) -> bool:
@@ -113,6 +114,9 @@ def from_env(cls) -> "ConfluenceConfig":
113114
no_proxy = os.getenv("CONFLUENCE_NO_PROXY", os.getenv("NO_PROXY"))
114115
socks_proxy = os.getenv("CONFLUENCE_SOCKS_PROXY", os.getenv("SOCKS_PROXY"))
115116

117+
# Custom headers - service-specific only
118+
custom_headers = get_custom_headers("CONFLUENCE_CUSTOM_HEADERS")
119+
116120
return cls(
117121
url=url,
118122
auth_type=auth_type,
@@ -126,6 +130,7 @@ def from_env(cls) -> "ConfluenceConfig":
126130
https_proxy=https_proxy,
127131
no_proxy=no_proxy,
128132
socks_proxy=socks_proxy,
133+
custom_headers=custom_headers,
129134
)
130135

131136
def is_auth_configured(self) -> bool:

src/mcp_atlassian/jira/client.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,10 @@ def __init__(self, config: JiraConfig | None = None) -> None:
128128
os.environ["NO_PROXY"] = self.config.no_proxy
129129
log_config_param(logger, "Jira", "NO_PROXY", self.config.no_proxy)
130130

131+
# Apply custom headers if configured
132+
if self.config.custom_headers:
133+
self._apply_custom_headers()
134+
131135
# Initialize the text preprocessor for text processing capabilities
132136
self.preprocessor = JiraPreprocessor(base_url=self.config.url)
133137
self._field_ids_cache = None
@@ -170,6 +174,18 @@ def _validate_authentication(self) -> None:
170174
)
171175
raise MCPAtlassianAuthenticationError(error_msg) from e
172176

177+
def _apply_custom_headers(self) -> None:
178+
"""Apply custom headers to the Jira session."""
179+
if not self.config.custom_headers:
180+
return
181+
182+
logger.debug(
183+
f"Applying {len(self.config.custom_headers)} custom headers to Jira session"
184+
)
185+
for header_name, header_value in self.config.custom_headers.items():
186+
self.jira._session.headers[header_name] = header_value
187+
logger.debug(f"Applied custom header: {header_name}")
188+
173189
def _clean_text(self, text: str) -> str:
174190
"""Clean text content by:
175191
1. Processing user mentions and links

src/mcp_atlassian/jira/config.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from dataclasses import dataclass
66
from typing import Literal
77

8-
from ..utils.env import is_env_ssl_verify
8+
from ..utils.env import get_custom_headers, is_env_ssl_verify
99
from ..utils.oauth import (
1010
BYOAccessTokenOAuthConfig,
1111
OAuthConfig,
@@ -35,6 +35,7 @@ class JiraConfig:
3535
https_proxy: str | None = None # HTTPS proxy URL
3636
no_proxy: str | None = None # Comma-separated list of hosts to bypass proxy
3737
socks_proxy: str | None = None # SOCKS proxy URL (optional)
38+
custom_headers: dict[str, str] | None = None # Custom HTTP headers
3839

3940
@property
4041
def is_cloud(self) -> bool:
@@ -113,6 +114,9 @@ def from_env(cls) -> "JiraConfig":
113114
no_proxy = os.getenv("JIRA_NO_PROXY", os.getenv("NO_PROXY"))
114115
socks_proxy = os.getenv("JIRA_SOCKS_PROXY", os.getenv("SOCKS_PROXY"))
115116

117+
# Custom headers - service-specific only
118+
custom_headers = get_custom_headers("JIRA_CUSTOM_HEADERS")
119+
116120
return cls(
117121
url=url,
118122
auth_type=auth_type,
@@ -126,6 +130,7 @@ def from_env(cls) -> "JiraConfig":
126130
https_proxy=https_proxy,
127131
no_proxy=no_proxy,
128132
socks_proxy=socks_proxy,
133+
custom_headers=custom_headers,
129134
)
130135

131136
def is_auth_configured(self) -> bool:

src/mcp_atlassian/utils/env.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,45 @@ def is_env_ssl_verify(env_var_name: str, default: str = "true") -> bool:
4949
True unless explicitly set to false values
5050
"""
5151
return os.getenv(env_var_name, default).lower() not in ("false", "0", "no")
52+
53+
54+
def get_custom_headers(env_var_name: str) -> dict[str, str]:
55+
"""Parse custom headers from environment variable containing comma-separated key=value pairs.
56+
57+
Args:
58+
env_var_name: Name of the environment variable to read
59+
60+
Returns:
61+
Dictionary of parsed headers
62+
63+
Examples:
64+
>>> # With CUSTOM_HEADERS="X-Custom=value1,X-Other=value2"
65+
>>> parse_custom_headers("CUSTOM_HEADERS")
66+
{'X-Custom': 'value1', 'X-Other': 'value2'}
67+
>>> # With unset environment variable
68+
>>> parse_custom_headers("UNSET_VAR")
69+
{}
70+
"""
71+
header_string = os.getenv(env_var_name)
72+
if not header_string or not header_string.strip():
73+
return {}
74+
75+
headers = {}
76+
pairs = header_string.split(",")
77+
78+
for pair in pairs:
79+
pair = pair.strip()
80+
if not pair:
81+
continue
82+
83+
if "=" not in pair:
84+
continue
85+
86+
key, value = pair.split("=", 1) # Split on first = only
87+
key = key.strip()
88+
value = value.strip()
89+
90+
if key: # Only add if key is not empty
91+
headers[key] = value
92+
93+
return headers

0 commit comments

Comments
 (0)