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
This commit is contained in:
Roo Code 2025-09-28 18:43:10 +00:00
parent a57528dc8e
commit a158c21e8c
5 changed files with 387 additions and 6 deletions

1
.review/pr-8274 Submodule

@ -0,0 +1 @@
Subproject commit e46929b8d8add0cd3c412d69f8ac882c405a4ba9

View file

@ -0,0 +1,167 @@
# MCP Server Certificate Trust Configuration
This document describes how to configure certificate trust settings for MCP servers that use HTTPS connections (SSE and StreamableHTTP transports).
## Overview
When connecting to MCP servers over HTTPS, you may encounter servers that use:
- Self-signed certificates
- Certificates signed by internal/corporate Certificate Authorities (CAs)
- Certificates that would otherwise be rejected by default Node.js certificate validation
The certificate trust configuration allows you to specify how these certificates should be handled.
## Configuration Options
Certificate trust settings can be added to any SSE or StreamableHTTP server configuration in your MCP settings file.
### Available Options
| Option | Type | Default | Description |
| -------------------- | ------- | --------- | ----------------------------------------------------------- |
| `allowSelfSigned` | boolean | false | Allow connections to servers using self-signed certificates |
| `caCertPath` | string | undefined | Path to a CA certificate file (PEM format) to trust |
| `rejectUnauthorized` | boolean | true | Whether to reject unauthorized certificates |
## Configuration Examples
### 1. Allow Self-Signed Certificates
```json
{
"mcpServers": {
"my-internal-server": {
"type": "sse",
"url": "https://internal.company.com/mcp",
"certificateTrust": {
"allowSelfSigned": true
}
}
}
}
```
### 2. Trust a Custom CA Certificate
```json
{
"mcpServers": {
"corporate-server": {
"type": "streamable-http",
"url": "https://api.internal.corp/mcp",
"certificateTrust": {
"caCertPath": "/path/to/company-ca.pem"
}
}
}
}
```
### 3. Disable Certificate Validation (Development Only)
⚠️ **Warning**: This configuration disables certificate validation entirely and should only be used in development environments.
```json
{
"mcpServers": {
"dev-server": {
"type": "sse",
"url": "https://dev.local:8443/mcp",
"certificateTrust": {
"rejectUnauthorized": false
}
}
}
}
```
### 4. Combined Configuration
```json
{
"mcpServers": {
"complex-server": {
"type": "sse",
"url": "https://secure.internal.com/mcp",
"headers": {
"Authorization": "Bearer token"
},
"certificateTrust": {
"allowSelfSigned": true,
"caCertPath": "/etc/ssl/certs/internal-ca.pem",
"rejectUnauthorized": false
}
}
}
}
```
## Obtaining CA Certificates
### From System Certificate Store
On many systems, CA certificates are stored in standard locations:
- **Linux**: `/etc/ssl/certs/` or `/usr/share/ca-certificates/`
- **macOS**: Can be exported from Keychain Access
- **Windows**: Can be exported from Certificate Manager (certmgr.msc)
### From Your IT Department
For corporate environments, contact your IT department to obtain:
1. The internal CA certificate (usually in PEM or CRT format)
2. Instructions on where to save it securely
3. Any specific certificate validation requirements
### Converting Certificate Formats
If you have a certificate in DER/CER format, convert it to PEM:
```bash
openssl x509 -inform der -in certificate.cer -out certificate.pem
```
## Security Considerations
1. **Production Environments**: Always use proper certificates signed by trusted CAs in production.
2. **Certificate Validation**: Only disable `rejectUnauthorized` in development environments where security is not a concern.
3. **CA Certificate Storage**: Store CA certificate files in a secure location with appropriate file permissions.
4. **Regular Updates**: Keep CA certificates up to date, especially for internal CAs that may rotate periodically.
## Troubleshooting
### Common Error Messages
1. **"UNABLE_TO_VERIFY_LEAF_SIGNATURE"**: The server's certificate cannot be verified. Consider adding the CA certificate using `caCertPath`.
2. **"SELF_SIGNED_CERT_IN_CHAIN"**: The certificate chain contains a self-signed certificate. Set `allowSelfSigned: true` if this is expected.
3. **"CERT_HAS_EXPIRED"**: The certificate has expired. Contact the server administrator to renew it.
### Debugging Certificate Issues
To debug certificate issues, you can test the connection using OpenSSL:
```bash
# View server certificate
openssl s_client -connect hostname:port -showcerts
# Test with a specific CA certificate
openssl s_client -connect hostname:port -CAfile /path/to/ca.pem
```
## Limitations
- Certificate trust settings only apply to SSE and StreamableHTTP transports
- STDIO transport servers do not use HTTPS and therefore don't need certificate configuration
- The configuration requires Node.js environment; browser-based implementations may have different requirements
## Related Documentation
- [MCP Server Configuration](./mcp-servers.md)
- [Model Context Protocol Specification](https://modelcontextprotocol.io)

View file

@ -65,6 +65,16 @@ const BaseConfigSchema = z.object({
disabledTools: z.array(z.string()).default([]),
})
// Certificate trust configuration schema
const CertificateTrustSchema = z.object({
// Allow self-signed certificates
allowSelfSigned: z.boolean().optional(),
// Path to CA certificate file (PEM format)
caCertPath: z.string().optional(),
// Reject unauthorized certificates (default: true for security)
rejectUnauthorized: z.boolean().optional().default(true),
})
// Custom error messages for better user feedback
const typeErrorMessage = "Server type must be 'stdio', 'sse', or 'streamable-http'"
const stdioFieldsErrorMessage =
@ -102,6 +112,8 @@ const createServerTypeSchema = () => {
type: z.enum(["sse"]).optional(),
url: z.string().url("URL must be a valid URL format"),
headers: z.record(z.string()).optional(),
// Certificate trust configuration for HTTPS connections
certificateTrust: CertificateTrustSchema.optional(),
// Ensure no stdio fields are present
command: z.undefined().optional(),
args: z.undefined().optional(),
@ -117,6 +129,8 @@ const createServerTypeSchema = () => {
type: z.enum(["streamable-http"]).optional(),
url: z.string().url("URL must be a valid URL format"),
headers: z.record(z.string()).optional(),
// Certificate trust configuration for HTTPS connections
certificateTrust: CertificateTrustSchema.optional(),
// Ensure no stdio fields are present
command: z.undefined().optional(),
args: z.undefined().optional(),
@ -735,10 +749,47 @@ export class McpHub {
}
} else if (configInjected.type === "streamable-http") {
// Streamable HTTP connection
const requestInit: RequestInit = {
headers: configInjected.headers,
}
// Apply certificate trust settings if configured
if (configInjected.certificateTrust) {
const { allowSelfSigned, caCertPath, rejectUnauthorized } = configInjected.certificateTrust
// For Node.js fetch, we need to configure the agent
if (typeof process !== "undefined" && process.versions && process.versions.node) {
const https = await import("https")
const fs = await import("fs/promises")
const agentOptions: any = {}
// Handle certificate rejection
if (rejectUnauthorized === false || allowSelfSigned === true) {
agentOptions.rejectUnauthorized = false
}
// Load CA certificate if provided
if (caCertPath) {
try {
const caCert = await fs.readFile(caCertPath, "utf-8")
agentOptions.ca = caCert
} catch (error) {
console.error(`Failed to load CA certificate from ${caCertPath}:`, error)
throw new Error(
`Failed to load CA certificate: ${error instanceof Error ? error.message : String(error)}`,
)
}
}
// Create HTTPS agent with certificate trust settings
const agent = new https.Agent(agentOptions)
;(requestInit as any).agent = agent
}
}
transport = new StreamableHTTPClientTransport(new URL(configInjected.url), {
requestInit: {
headers: configInjected.headers,
},
requestInit,
})
// Set up Streamable HTTP specific error handling
@ -766,18 +817,66 @@ export class McpHub {
headers: configInjected.headers,
},
}
// Configure ReconnectingEventSource options
const reconnectingEventSourceOptions = {
const reconnectingEventSourceOptions: any = {
max_retry_time: 5000, // Maximum retry time in milliseconds
withCredentials: configInjected.headers?.["Authorization"] ? true : false, // Enable credentials if Authorization header exists
fetch: (url: string | URL, init: RequestInit) => {
}
// Apply certificate trust settings if configured
if (configInjected.certificateTrust) {
const { allowSelfSigned, caCertPath, rejectUnauthorized } = configInjected.certificateTrust
// For Node.js fetch used by ReconnectingEventSource
if (typeof process !== "undefined" && process.versions && process.versions.node) {
const https = await import("https")
const fs = await import("fs/promises")
const agentOptions: any = {}
// Handle certificate rejection
if (rejectUnauthorized === false || allowSelfSigned === true) {
agentOptions.rejectUnauthorized = false
}
// Load CA certificate if provided
if (caCertPath) {
try {
const caCert = await fs.readFile(caCertPath, "utf-8")
agentOptions.ca = caCert
} catch (error) {
console.error(`Failed to load CA certificate from ${caCertPath}:`, error)
throw new Error(
`Failed to load CA certificate: ${error instanceof Error ? error.message : String(error)}`,
)
}
}
// Create HTTPS agent with certificate trust settings
const agent = new https.Agent(agentOptions)
// Custom fetch function that includes the HTTPS agent
reconnectingEventSourceOptions.fetch = (url: string | URL, init: RequestInit) => {
const headers = new Headers({ ...(init?.headers || {}), ...(configInjected.headers || {}) })
return fetch(url, {
...init,
headers,
agent: agent as any,
})
}
}
} else {
// Default fetch function without certificate trust modifications
reconnectingEventSourceOptions.fetch = (url: string | URL, init: RequestInit) => {
const headers = new Headers({ ...(init?.headers || {}), ...(configInjected.headers || {}) })
return fetch(url, {
...init,
headers,
})
},
}
}
global.EventSource = ReconnectingEventSource
transport = new SSEClientTransport(new URL(configInjected.url), {
...sseOptions,

View file

@ -1798,6 +1798,119 @@ describe("McpHub", () => {
})
})
describe("Certificate trust configuration", () => {
it("should accept SSE server with certificate trust configuration", () => {
const config = {
type: "sse",
url: "https://api.example.com/mcp",
headers: { Authorization: "Bearer token" },
certificateTrust: {
allowSelfSigned: true,
caCertPath: "/path/to/ca.pem",
rejectUnauthorized: false,
},
}
const result = ServerConfigSchema.safeParse(config)
expect(result.success).toBe(true)
if (result.success && result.data.type === "sse") {
expect(result.data.type).toBe("sse")
expect(result.data.certificateTrust).toEqual({
allowSelfSigned: true,
caCertPath: "/path/to/ca.pem",
rejectUnauthorized: false,
})
}
})
it("should accept StreamableHTTP server with certificate trust configuration", () => {
const config = {
type: "streamable-http",
url: "https://api.example.com/mcp",
headers: { Authorization: "Bearer token" },
certificateTrust: {
allowSelfSigned: false,
rejectUnauthorized: true,
},
}
const result = ServerConfigSchema.safeParse(config)
expect(result.success).toBe(true)
if (result.success && result.data.type === "streamable-http") {
expect(result.data.type).toBe("streamable-http")
expect(result.data.certificateTrust).toEqual({
allowSelfSigned: false,
rejectUnauthorized: true,
})
}
})
it("should accept SSE server with only CA certificate path", () => {
const config = {
type: "sse",
url: "https://api.example.com/mcp",
certificateTrust: {
caCertPath: "/path/to/ca.pem",
},
}
const result = ServerConfigSchema.safeParse(config)
expect(result.success).toBe(true)
if (result.success && result.data.type === "sse") {
expect(result.data.certificateTrust?.caCertPath).toBe("/path/to/ca.pem")
expect(result.data.certificateTrust?.rejectUnauthorized).toBe(true) // default value
}
})
it("should accept server without certificate trust configuration", () => {
const config = {
type: "sse",
url: "https://api.example.com/mcp",
}
const result = ServerConfigSchema.safeParse(config)
expect(result.success).toBe(true)
if (result.success && result.data.type === "sse") {
expect(result.data.certificateTrust).toBeUndefined()
}
})
it("should not accept certificate trust for stdio servers", () => {
const config = {
type: "stdio",
command: "node",
args: ["server.js"],
certificateTrust: {
allowSelfSigned: true,
},
}
// Note: stdio schema doesn't include certificateTrust, so it will be stripped
const result = ServerConfigSchema.safeParse(config)
expect(result.success).toBe(true)
if (result.success) {
expect(result.data.type).toBe("stdio")
expect((result.data as any).certificateTrust).toBeUndefined()
}
})
it("should default rejectUnauthorized to true when not specified", () => {
const config = {
type: "sse",
url: "https://api.example.com/mcp",
certificateTrust: {
allowSelfSigned: true,
},
}
const result = ServerConfigSchema.safeParse(config)
expect(result.success).toBe(true)
if (result.success && result.data.type === "sse") {
expect(result.data.certificateTrust?.rejectUnauthorized).toBe(true)
}
})
})
describe("Windows command wrapping", () => {
let StdioClientTransport: ReturnType<typeof vi.fn>
let Client: ReturnType<typeof vi.fn>

1
tmp/pr-8287-Roo-Code Submodule

@ -0,0 +1 @@
Subproject commit 88a473b017af37091c85ce3056e444e856f80d6e