-
Notifications
You must be signed in to change notification settings - Fork 1
feat: add certificate trust configuration for MCP servers #5
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
a158c21
b73bd7c
e2b5556
2ad3209
c533ebb
ff458a5
d04abc0
a11aad8
a57d035
6fa6924
5922c94
cf6abc7
6f1b0c5
4a963a1
6a0fbdc
91530f9
39dbeee
afa5eb9
1cfb8da
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,168 @@ | ||
| # 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 | ||
| - Test webhook commit 17 | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This test commit message should be removed before merging the PR. |
||
|
|
||
| ## Related Documentation | ||
|
|
||
| - [MCP Server Configuration](./mcp-servers.md) | ||
| - [Model Context Protocol Specification](https://modelcontextprotocol.io) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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) | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Node 18+ global fetch (undici) ignores RequestInit.agent, so these certificateTrust settings won’t take effect for StreamableHTTP. Use undici’s dispatcher instead. Suggest: create an undici Agent with TLS options (e.g., new Agent({ connect: { tls: { rejectUnauthorized: false, ca: caCert } } })) and set (requestInit as any).dispatcher = agent. This also applies to the SSE custom fetch. |
||
| ;(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,67 @@ 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 || {}) }) | ||
| // Use type assertion for Node.js-specific agent property | ||
| return fetch(url, { | ||
| ...init, | ||
| headers, | ||
| ...({ agent } as any), | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. SSE path: the custom fetch adds |
||
| }) | ||
| } | ||
| } | ||
| } 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, | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Limitation note: In Node 18+ the global fetch is powered by undici and ignores RequestInit.agent. For certificate overrides (custom CA, self-signed, disabling verification) you must use undici’s dispatcher (e.g., an undici Agent/Pool with TLS options) and pass it via fetch(..., { dispatcher }). Consider adding this to the Limitations with a short example so users don’t expect
agentto work in Node.