Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
3f7ca94
feat(mcp): per-server TLS trust configuration
karup Aug 2, 2026
4ae4aa4
refactor(mcp): consolidate TLS building into shared helper, fix revie…
karup Aug 2, 2026
3d14e9c
fix(mcp): close TOCTOU window in readCaFile via fd-based atomic read
karup Aug 2, 2026
53a68d1
docs: add TLS trust configuration to MCP servers documentation
karup Aug 2, 2026
2542f22
test: fix TLS test assertions for trimmed PEM and line slicing
karup Aug 2, 2026
7ba8c99
Merge branch 'dev' into mcp-tls-trust
karup Aug 3, 2026
0cc8073
Merge branch 'dev' into mcp-tls-trust
karup Aug 3, 2026
90ace0b
Merge branch 'dev' into mcp-tls-trust
karup Aug 4, 2026
62a40a9
Merge branch 'dev' into mcp-tls-trust
karup Aug 4, 2026
931e02d
Merge branch 'dev' into mcp-tls-trust
karup Aug 6, 2026
eb0783c
Merge branch 'dev' into mcp-tls-trust
karup Aug 7, 2026
89e2239
Merge branch 'dev' into mcp-tls-trust
karup Aug 7, 2026
a413211
Merge branch 'dev' into mcp-tls-trust
karup Aug 7, 2026
3b63ab8
Merge branch 'dev' into mcp-tls-trust
karup Aug 7, 2026
4e441ef
Merge branch 'dev' into mcp-tls-trust
karup Aug 8, 2026
6ecfa00
Merge branch 'dev' into mcp-tls-trust
karup Aug 12, 2026
e0ee22c
Merge branch 'dev' into mcp-tls-trust
karup Aug 13, 2026
493b8a1
Merge branch 'dev' into mcp-tls-trust
karup Aug 14, 2026
16b090a
Merge branch 'dev' into mcp-tls-trust
karup Aug 18, 2026
a729c51
Merge branch 'dev' into mcp-tls-trust
karup Aug 20, 2026
1f617c1
Merge branch 'dev' into mcp-tls-trust
karup Aug 20, 2026
e792a0c
Merge branch 'dev' into mcp-tls-trust
karup Aug 20, 2026
c325ab3
Merge branch 'dev' into mcp-tls-trust
karup Aug 20, 2026
6bdcc98
Merge branch 'dev' into mcp-tls-trust
karup Aug 20, 2026
0f09ed0
Merge branch 'dev' into mcp-tls-trust
karup Aug 22, 2026
914c777
Merge branch 'dev' into mcp-tls-trust
karup Aug 22, 2026
76f09bd
Merge branch 'dev' into mcp-tls-trust
karup Aug 26, 2026
5bd248a
Merge branch 'dev' into mcp-tls-trust
karup Aug 28, 2026
ae30c61
Merge branch 'dev' into mcp-tls-trust
karup Aug 28, 2026
6126e92
Merge branch 'dev' into mcp-tls-trust
karup Aug 29, 2026
8e7185b
Merge branch 'dev' into mcp-tls-trust
karup Aug 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions packages/core/src/config/mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,32 @@ export class OAuth extends Schema.Class<OAuth>("ConfigV2.MCP.OAuth")({
redirect_uri: Schema.String.pipe(Schema.optional),
}) {}

export class Tls extends Schema.Class<Tls>("ConfigV2.MCP.Tls")({
ca_file: Schema.String.pipe(Schema.optional).annotate({
description:
"Path to a custom CA certificate file (PEM format) to trust when connecting to this MCP server.",
}),
ca_pem: Schema.String.pipe(Schema.optional).annotate({
description:
"Custom CA certificate content (PEM format) to trust when connecting to this MCP server.",
}),
fingerprint: Schema.String.pipe(Schema.optional).annotate({
description:
"SHA256 fingerprint of the server certificate to trust. Format: 'SHA256:XX:XX:...' or 'XX:XX:...'.",
}),
}) {}

export class Remote extends Schema.Class<Remote>("ConfigV2.MCP.Remote")({
type: Schema.Literal("remote"),
url: Schema.String,
headers: Schema.Record(Schema.String, Schema.String).pipe(Schema.optional),
oauth: Schema.Union([OAuth, Schema.Literal(false)]).pipe(Schema.optional),
disabled: Schema.Boolean.pipe(Schema.optional),
timeout: Timeout.pipe(Schema.optional),
tls: Tls.pipe(Schema.optional).annotate({
description:
"TLS trust configuration for this MCP server. Use to trust self-signed certificates, private CAs, or pin specific certificates.",
}),
}) {}

export const Server = Schema.Union([Local, Remote]).pipe(Schema.toTaggedUnion("type"))
Expand Down
19 changes: 19 additions & 0 deletions packages/core/src/v1/config/mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,22 @@ export const OAuth = Schema.Struct({
}).annotate({ identifier: "McpOAuthConfig" })
export type OAuth = Schema.Schema.Type<typeof OAuth>

export const Tls = Schema.Struct({
caFile: Schema.optional(Schema.String).annotate({
description:
"Path to a custom CA certificate file (PEM format) to trust when connecting to this MCP server. Only applies to this server; does not affect global TLS.",
}),
caPem: Schema.optional(Schema.String).annotate({
description:
"Custom CA certificate content (PEM format) to trust when connecting to this MCP server. Useful for self-contained configurations where a separate file is impractical.",
}),
fingerprint: Schema.optional(Schema.String).annotate({
description:
"SHA256 fingerprint of the server certificate to trust. Format: 'SHA256:XX:XX:...' or 'XX:XX:...'. The client verifies that the server certificate matches before trusting. Similar to SSH host key verification.",
}),
}).annotate({ identifier: "McpTlsConfig" })
export type Tls = Schema.Schema.Type<typeof Tls>

export const Remote = Schema.Struct({
type: Schema.Literal("remote").annotate({ description: "Type of MCP server connection" }),
url: Schema.String.annotate({ description: "URL of the remote MCP server" }),
Expand All @@ -56,6 +72,9 @@ export const Remote = Schema.Struct({
timeout: Schema.optional(PositiveInt).annotate({
description: "Timeout in ms for MCP server requests. Defaults to 5000 (5 seconds) if not specified.",
}),
tls: Schema.optional(Tls).annotate({
description: "TLS trust configuration for this MCP server. Use to trust self-signed certificates, private CAs, or pin specific certificates.",
}),
}).annotate({ identifier: "McpRemoteConfig" })
export type Remote = Schema.Schema.Type<typeof Remote>

Expand Down
16 changes: 15 additions & 1 deletion packages/opencode/src/cli/cmd/mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import { McpOAuthProvider } from "../../mcp/oauth-provider"
import { Config } from "@/config/config"
import { ConfigMCPV1 } from "@opencode-ai/core/v1/config/mcp"
import { InstanceRef } from "@/effect/instance-ref"
import { InstanceState } from "@/effect/instance-state"
import { buildTlsCa, createTlsFetch } from "../../mcp/tls"
import { InstallationVersion } from "@opencode-ai/core/installation/version"
import path from "path"
import { Global } from "@opencode-ai/core/global"
Expand Down Expand Up @@ -677,6 +679,17 @@ export const McpDebugCommand = effectCmd({
entry: auth.get(args.name),
})
: undefined
let tlsFetch: typeof fetch | undefined
if (serverConfig && isMcpRemote(serverConfig) && serverConfig.tls) {
const directory = yield* InstanceState.directory
const url = new URL(serverConfig.url)
const result = yield* Effect.tryPromise({
try: () => buildTlsCa(serverConfig.tls, directory, url),
catch: (error) => (error instanceof Error ? error : new Error(String(error))),
})
if (result instanceof Error) throw new Error(result.message)
if (result) tlsFetch = createTlsFetch(result)
}
yield* Effect.promise(async () => {
UI.empty()
prompts.intro("MCP OAuth Debug")
Expand Down Expand Up @@ -733,7 +746,7 @@ export const McpDebugCommand = effectCmd({

// Test basic HTTP connectivity first
try {
const response = await fetch(serverConfig.url, {
const response = await (tlsFetch ?? fetch)(serverConfig.url, {
method: "POST",
headers: {
...serverConfig.headers,
Expand Down Expand Up @@ -786,6 +799,7 @@ export const McpDebugCommand = effectCmd({
const transport = new StreamableHTTPClientTransport(new URL(serverConfig.url), {
authProvider,
requestInit: serverConfig.headers ? { headers: serverConfig.headers } : undefined,
...(tlsFetch ? { fetch: tlsFetch } : {}),
})

try {
Expand Down
33 changes: 33 additions & 0 deletions packages/opencode/src/mcp/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { McpCatalog } from "./catalog"
import { McpEvent } from "@opencode-ai/schema/mcp-event"
import { McpBrowser } from "./browser"
import { buildTlsCa, createTlsFetch } from "./tls"

const DEFAULT_TIMEOUT = 30_000
const CLIENT_OPTIONS = {
Expand Down Expand Up @@ -266,19 +267,38 @@ const layer = Layer.effect(
)
}

let tlsFetch: typeof fetch | undefined

if (mcp.tls) {
const directory = yield* InstanceState.directory
const result = yield* Effect.tryPromise({
try: () => buildTlsCa(mcp.tls, directory, url),
catch: (error) => (error instanceof Error ? error : new Error(String(error))),
})
if (result instanceof Error) {
return {
client: undefined as MCPClient | undefined,
status: { status: "failed" as const, error: result.message },
}
}
if (result) tlsFetch = createTlsFetch(result)
}

const transports: Array<{ name: string; transport: TransportWithAuth }> = [
{
name: "StreamableHTTP",
transport: new StreamableHTTPClientTransport(url, {
authProvider,
requestInit: mcp.headers ? { headers: mcp.headers } : undefined,
...(tlsFetch ? { fetch: tlsFetch } : {}),
}),
},
{
name: "SSE",
transport: new SSEClientTransport(url, {
authProvider,
requestInit: mcp.headers ? { headers: mcp.headers } : undefined,
...(tlsFetch ? { fetch: tlsFetch } : {}),
}),
},
]
Expand Down Expand Up @@ -843,9 +863,22 @@ const layer = Layer.effect(
auth,
)

let tlsFetch: typeof fetch | undefined

if (mcpConfig.tls) {
const directory = yield* InstanceState.directory
const result = yield* Effect.tryPromise({
try: () => buildTlsCa(mcpConfig.tls, directory, url),
catch: (error) => (error instanceof Error ? error : new Error(String(error))),
})
if (result instanceof Error) throw new Error(result.message)
if (result) tlsFetch = createTlsFetch(result)
}

const transport = new StreamableHTTPClientTransport(url, {
authProvider,
requestInit: mcpConfig.headers ? { headers: mcpConfig.headers } : undefined,
...(tlsFetch ? { fetch: tlsFetch } : {}),
})
const directory = yield* InstanceState.directory

Expand Down
Loading
Loading