Skip to content

Commit a158c21

Browse files
committed
feat: add certificate trust configuration for MCP servers
- Add certificateTrust configuration options for SSE and StreamableHTTP transports - Support allowSelfSigned, caCertPath, and rejectUnauthorized options - Implement HTTPS agent configuration for Node.js fetch operations - Add comprehensive tests for certificate trust configuration - Add documentation explaining usage and security considerations Fixes #8355
1 parent a57528d commit a158c21

File tree

5 files changed

+387
-6
lines changed

5 files changed

+387
-6
lines changed

.review/pr-8274

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Subproject commit e46929b8d8add0cd3c412d69f8ac882c405a4ba9

docs/mcp-certificate-trust.md

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
# MCP Server Certificate Trust Configuration
2+
3+
This document describes how to configure certificate trust settings for MCP servers that use HTTPS connections (SSE and StreamableHTTP transports).
4+
5+
## Overview
6+
7+
When connecting to MCP servers over HTTPS, you may encounter servers that use:
8+
9+
- Self-signed certificates
10+
- Certificates signed by internal/corporate Certificate Authorities (CAs)
11+
- Certificates that would otherwise be rejected by default Node.js certificate validation
12+
13+
The certificate trust configuration allows you to specify how these certificates should be handled.
14+
15+
## Configuration Options
16+
17+
Certificate trust settings can be added to any SSE or StreamableHTTP server configuration in your MCP settings file.
18+
19+
### Available Options
20+
21+
| Option | Type | Default | Description |
22+
| -------------------- | ------- | --------- | ----------------------------------------------------------- |
23+
| `allowSelfSigned` | boolean | false | Allow connections to servers using self-signed certificates |
24+
| `caCertPath` | string | undefined | Path to a CA certificate file (PEM format) to trust |
25+
| `rejectUnauthorized` | boolean | true | Whether to reject unauthorized certificates |
26+
27+
## Configuration Examples
28+
29+
### 1. Allow Self-Signed Certificates
30+
31+
```json
32+
{
33+
"mcpServers": {
34+
"my-internal-server": {
35+
"type": "sse",
36+
"url": "https://internal.company.com/mcp",
37+
"certificateTrust": {
38+
"allowSelfSigned": true
39+
}
40+
}
41+
}
42+
}
43+
```
44+
45+
### 2. Trust a Custom CA Certificate
46+
47+
```json
48+
{
49+
"mcpServers": {
50+
"corporate-server": {
51+
"type": "streamable-http",
52+
"url": "https://api.internal.corp/mcp",
53+
"certificateTrust": {
54+
"caCertPath": "/path/to/company-ca.pem"
55+
}
56+
}
57+
}
58+
}
59+
```
60+
61+
### 3. Disable Certificate Validation (Development Only)
62+
63+
⚠️ **Warning**: This configuration disables certificate validation entirely and should only be used in development environments.
64+
65+
```json
66+
{
67+
"mcpServers": {
68+
"dev-server": {
69+
"type": "sse",
70+
"url": "https://dev.local:8443/mcp",
71+
"certificateTrust": {
72+
"rejectUnauthorized": false
73+
}
74+
}
75+
}
76+
}
77+
```
78+
79+
### 4. Combined Configuration
80+
81+
```json
82+
{
83+
"mcpServers": {
84+
"complex-server": {
85+
"type": "sse",
86+
"url": "https://secure.internal.com/mcp",
87+
"headers": {
88+
"Authorization": "Bearer token"
89+
},
90+
"certificateTrust": {
91+
"allowSelfSigned": true,
92+
"caCertPath": "/etc/ssl/certs/internal-ca.pem",
93+
"rejectUnauthorized": false
94+
}
95+
}
96+
}
97+
}
98+
```
99+
100+
## Obtaining CA Certificates
101+
102+
### From System Certificate Store
103+
104+
On many systems, CA certificates are stored in standard locations:
105+
106+
- **Linux**: `/etc/ssl/certs/` or `/usr/share/ca-certificates/`
107+
- **macOS**: Can be exported from Keychain Access
108+
- **Windows**: Can be exported from Certificate Manager (certmgr.msc)
109+
110+
### From Your IT Department
111+
112+
For corporate environments, contact your IT department to obtain:
113+
114+
1. The internal CA certificate (usually in PEM or CRT format)
115+
2. Instructions on where to save it securely
116+
3. Any specific certificate validation requirements
117+
118+
### Converting Certificate Formats
119+
120+
If you have a certificate in DER/CER format, convert it to PEM:
121+
122+
```bash
123+
openssl x509 -inform der -in certificate.cer -out certificate.pem
124+
```
125+
126+
## Security Considerations
127+
128+
1. **Production Environments**: Always use proper certificates signed by trusted CAs in production.
129+
130+
2. **Certificate Validation**: Only disable `rejectUnauthorized` in development environments where security is not a concern.
131+
132+
3. **CA Certificate Storage**: Store CA certificate files in a secure location with appropriate file permissions.
133+
134+
4. **Regular Updates**: Keep CA certificates up to date, especially for internal CAs that may rotate periodically.
135+
136+
## Troubleshooting
137+
138+
### Common Error Messages
139+
140+
1. **"UNABLE_TO_VERIFY_LEAF_SIGNATURE"**: The server's certificate cannot be verified. Consider adding the CA certificate using `caCertPath`.
141+
142+
2. **"SELF_SIGNED_CERT_IN_CHAIN"**: The certificate chain contains a self-signed certificate. Set `allowSelfSigned: true` if this is expected.
143+
144+
3. **"CERT_HAS_EXPIRED"**: The certificate has expired. Contact the server administrator to renew it.
145+
146+
### Debugging Certificate Issues
147+
148+
To debug certificate issues, you can test the connection using OpenSSL:
149+
150+
```bash
151+
# View server certificate
152+
openssl s_client -connect hostname:port -showcerts
153+
154+
# Test with a specific CA certificate
155+
openssl s_client -connect hostname:port -CAfile /path/to/ca.pem
156+
```
157+
158+
## Limitations
159+
160+
- Certificate trust settings only apply to SSE and StreamableHTTP transports
161+
- STDIO transport servers do not use HTTPS and therefore don't need certificate configuration
162+
- The configuration requires Node.js environment; browser-based implementations may have different requirements
163+
164+
## Related Documentation
165+
166+
- [MCP Server Configuration](./mcp-servers.md)
167+
- [Model Context Protocol Specification](https://modelcontextprotocol.io)

src/services/mcp/McpHub.ts

Lines changed: 105 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,16 @@ const BaseConfigSchema = z.object({
6565
disabledTools: z.array(z.string()).default([]),
6666
})
6767

68+
// Certificate trust configuration schema
69+
const CertificateTrustSchema = z.object({
70+
// Allow self-signed certificates
71+
allowSelfSigned: z.boolean().optional(),
72+
// Path to CA certificate file (PEM format)
73+
caCertPath: z.string().optional(),
74+
// Reject unauthorized certificates (default: true for security)
75+
rejectUnauthorized: z.boolean().optional().default(true),
76+
})
77+
6878
// Custom error messages for better user feedback
6979
const typeErrorMessage = "Server type must be 'stdio', 'sse', or 'streamable-http'"
7080
const stdioFieldsErrorMessage =
@@ -102,6 +112,8 @@ const createServerTypeSchema = () => {
102112
type: z.enum(["sse"]).optional(),
103113
url: z.string().url("URL must be a valid URL format"),
104114
headers: z.record(z.string()).optional(),
115+
// Certificate trust configuration for HTTPS connections
116+
certificateTrust: CertificateTrustSchema.optional(),
105117
// Ensure no stdio fields are present
106118
command: z.undefined().optional(),
107119
args: z.undefined().optional(),
@@ -117,6 +129,8 @@ const createServerTypeSchema = () => {
117129
type: z.enum(["streamable-http"]).optional(),
118130
url: z.string().url("URL must be a valid URL format"),
119131
headers: z.record(z.string()).optional(),
132+
// Certificate trust configuration for HTTPS connections
133+
certificateTrust: CertificateTrustSchema.optional(),
120134
// Ensure no stdio fields are present
121135
command: z.undefined().optional(),
122136
args: z.undefined().optional(),
@@ -735,10 +749,47 @@ export class McpHub {
735749
}
736750
} else if (configInjected.type === "streamable-http") {
737751
// Streamable HTTP connection
752+
const requestInit: RequestInit = {
753+
headers: configInjected.headers,
754+
}
755+
756+
// Apply certificate trust settings if configured
757+
if (configInjected.certificateTrust) {
758+
const { allowSelfSigned, caCertPath, rejectUnauthorized } = configInjected.certificateTrust
759+
760+
// For Node.js fetch, we need to configure the agent
761+
if (typeof process !== "undefined" && process.versions && process.versions.node) {
762+
const https = await import("https")
763+
const fs = await import("fs/promises")
764+
765+
const agentOptions: any = {}
766+
767+
// Handle certificate rejection
768+
if (rejectUnauthorized === false || allowSelfSigned === true) {
769+
agentOptions.rejectUnauthorized = false
770+
}
771+
772+
// Load CA certificate if provided
773+
if (caCertPath) {
774+
try {
775+
const caCert = await fs.readFile(caCertPath, "utf-8")
776+
agentOptions.ca = caCert
777+
} catch (error) {
778+
console.error(`Failed to load CA certificate from ${caCertPath}:`, error)
779+
throw new Error(
780+
`Failed to load CA certificate: ${error instanceof Error ? error.message : String(error)}`,
781+
)
782+
}
783+
}
784+
785+
// Create HTTPS agent with certificate trust settings
786+
const agent = new https.Agent(agentOptions)
787+
;(requestInit as any).agent = agent
788+
}
789+
}
790+
738791
transport = new StreamableHTTPClientTransport(new URL(configInjected.url), {
739-
requestInit: {
740-
headers: configInjected.headers,
741-
},
792+
requestInit,
742793
})
743794

744795
// Set up Streamable HTTP specific error handling
@@ -766,18 +817,66 @@ export class McpHub {
766817
headers: configInjected.headers,
767818
},
768819
}
820+
769821
// Configure ReconnectingEventSource options
770-
const reconnectingEventSourceOptions = {
822+
const reconnectingEventSourceOptions: any = {
771823
max_retry_time: 5000, // Maximum retry time in milliseconds
772824
withCredentials: configInjected.headers?.["Authorization"] ? true : false, // Enable credentials if Authorization header exists
773-
fetch: (url: string | URL, init: RequestInit) => {
825+
}
826+
827+
// Apply certificate trust settings if configured
828+
if (configInjected.certificateTrust) {
829+
const { allowSelfSigned, caCertPath, rejectUnauthorized } = configInjected.certificateTrust
830+
831+
// For Node.js fetch used by ReconnectingEventSource
832+
if (typeof process !== "undefined" && process.versions && process.versions.node) {
833+
const https = await import("https")
834+
const fs = await import("fs/promises")
835+
836+
const agentOptions: any = {}
837+
838+
// Handle certificate rejection
839+
if (rejectUnauthorized === false || allowSelfSigned === true) {
840+
agentOptions.rejectUnauthorized = false
841+
}
842+
843+
// Load CA certificate if provided
844+
if (caCertPath) {
845+
try {
846+
const caCert = await fs.readFile(caCertPath, "utf-8")
847+
agentOptions.ca = caCert
848+
} catch (error) {
849+
console.error(`Failed to load CA certificate from ${caCertPath}:`, error)
850+
throw new Error(
851+
`Failed to load CA certificate: ${error instanceof Error ? error.message : String(error)}`,
852+
)
853+
}
854+
}
855+
856+
// Create HTTPS agent with certificate trust settings
857+
const agent = new https.Agent(agentOptions)
858+
859+
// Custom fetch function that includes the HTTPS agent
860+
reconnectingEventSourceOptions.fetch = (url: string | URL, init: RequestInit) => {
861+
const headers = new Headers({ ...(init?.headers || {}), ...(configInjected.headers || {}) })
862+
return fetch(url, {
863+
...init,
864+
headers,
865+
agent: agent as any,
866+
})
867+
}
868+
}
869+
} else {
870+
// Default fetch function without certificate trust modifications
871+
reconnectingEventSourceOptions.fetch = (url: string | URL, init: RequestInit) => {
774872
const headers = new Headers({ ...(init?.headers || {}), ...(configInjected.headers || {}) })
775873
return fetch(url, {
776874
...init,
777875
headers,
778876
})
779-
},
877+
}
780878
}
879+
781880
global.EventSource = ReconnectingEventSource
782881
transport = new SSEClientTransport(new URL(configInjected.url), {
783882
...sseOptions,

0 commit comments

Comments
 (0)