mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
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:
parent
a57528dc8e
commit
a158c21e8c
5 changed files with 387 additions and 6 deletions
1
.review/pr-8274
Submodule
1
.review/pr-8274
Submodule
|
|
@ -0,0 +1 @@
|
|||
Subproject commit e46929b8d8add0cd3c412d69f8ac882c405a4ba9
|
||||
167
docs/mcp-certificate-trust.md
Normal file
167
docs/mcp-certificate-trust.md
Normal 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)
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
1
tmp/pr-8287-Roo-Code
Submodule
|
|
@ -0,0 +1 @@
|
|||
Subproject commit 88a473b017af37091c85ce3056e444e856f80d6e
|
||||
Loading…
Add table
Reference in a new issue