Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 4 additions & 0 deletions packages/opencode/src/config/mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ export class Remote extends Schema.Class<Remote>("McpRemoteConfig")({
timeout: Schema.optional(Schema.Number).annotate({
description: "Timeout in ms for MCP server requests. Defaults to 5000 (5 seconds) if not specified.",
}),
insecure: Schema.optional(Schema.Boolean).annotate({
description:
"Disable TLS certificate verification for this remote MCP server. Only use for self-signed certificates in trusted environments.",
}),
}) {
static readonly zod = zod(this)
}
Expand Down
22 changes: 21 additions & 1 deletion packages/opencode/src/mcp/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,22 @@ import { EffectBridge } from "@/effect"
import { InstanceState } from "@/effect"
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner"
import { Agent } from "node:https"

const log = Log.create({ service: "mcp" })
const DEFAULT_TIMEOUT = 30_000

const insecureAgent = new Agent({ rejectUnauthorized: false })

/**
* Creates a fetch function that skips TLS certificate verification.
* Supports both Bun (via `tls` init option) and Node (via `agent`).
*/
function createInsecureFetch(): typeof globalThis.fetch {
return ((input: RequestInfo | URL, init?: RequestInit) =>
fetch(input, Object.assign({}, init, { tls: { rejectUnauthorized: false }, agent: insecureAgent }))) as typeof fetch
}

export const Resource = z
.object({
name: z.string(),
Expand Down Expand Up @@ -303,19 +315,24 @@ export const layer = Layer.effect(
)
}

const insecureFetch = mcp.insecure ? createInsecureFetch() : undefined
if (mcp.insecure) log.warn("TLS certificate verification disabled", { key })

const transports: Array<{ name: string; transport: TransportWithAuth }> = [
{
name: "StreamableHTTP",
transport: new StreamableHTTPClientTransport(new URL(mcp.url), {
authProvider,
requestInit: mcp.headers ? { headers: mcp.headers } : undefined,
fetch: insecureFetch,
}),
},
{
name: "SSE",
transport: new SSEClientTransport(new URL(mcp.url), {
authProvider,
requestInit: mcp.headers ? { headers: mcp.headers } : undefined,
fetch: insecureFetch,
}),
},
]
Expand Down Expand Up @@ -766,7 +783,10 @@ export const layer = Layer.effect(
auth,
)

const transport = new StreamableHTTPClientTransport(new URL(mcpConfig.url), { authProvider })
const transport = new StreamableHTTPClientTransport(new URL(mcpConfig.url), {
authProvider,
fetch: mcpConfig.insecure ? createInsecureFetch() : undefined,
})

return yield* Effect.tryPromise({
try: () => {
Expand Down
72 changes: 72 additions & 0 deletions packages/opencode/test/mcp/insecure.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { test, expect, mock, beforeEach } from "bun:test"
import { Effect } from "effect"
import type { MCP as MCPNS } from "../../src/mcp/index"

const transportCalls: Array<{ fetch?: unknown }> = []

void mock.module("@modelcontextprotocol/sdk/client/streamableHttp.js", () => ({
StreamableHTTPClientTransport: class {
constructor(_url: URL, options?: { fetch?: unknown }) {
transportCalls.push({ fetch: options?.fetch })
}
async start() {
throw new Error("Mock transport cannot connect")
}
},
}))

void mock.module("@modelcontextprotocol/sdk/client/sse.js", () => ({
SSEClientTransport: class {
constructor(_url: URL, options?: { fetch?: unknown }) {
transportCalls.push({ fetch: options?.fetch })
}
async start() {
throw new Error("Mock transport cannot connect")
}
},
}))

beforeEach(() => {
transportCalls.length = 0
})

const { MCP } = await import("../../src/mcp/index")
const { AppRuntime } = await import("../../src/effect/app-runtime")
const { Instance } = await import("../../src/project/instance")
const { tmpdir } = await import("../fixture/fixture")
const service = MCP.Service as unknown as Effect.Effect<MCPNS.Interface, never, never>

function addServer(name: string, insecure?: boolean) {
return Effect.gen(function* () {
const mcp = yield* service
yield* mcp
.add(name, { type: "remote", url: "https://example.com/mcp", oauth: false, insecure })
.pipe(Effect.catch(() => Effect.void))
})
}

test("insecure: true passes custom fetch to transports", async () => {
await using tmp = await tmpdir()
await Instance.provide({
directory: tmp.path,
fn: () => AppRuntime.runPromise(addServer("insecure-server", true)),
})

expect(transportCalls.length).toBeGreaterThanOrEqual(1)
for (const call of transportCalls) {
expect(typeof call.fetch).toBe("function")
}
})

test("insecure not set does not pass custom fetch", async () => {
await using tmp = await tmpdir()
await Instance.provide({
directory: tmp.path,
fn: () => AppRuntime.runPromise(addServer("secure-server")),
})

expect(transportCalls.length).toBeGreaterThanOrEqual(1)
for (const call of transportCalls) {
expect(call.fetch).toBeUndefined()
}
})
44 changes: 36 additions & 8 deletions packages/web/src/content/docs/mcp-servers.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -152,14 +152,15 @@ The `url` is the URL of the remote MCP server and with the `headers` option you

#### Options

| Option | Type | Required | Description |
| --------- | ------- | -------- | ----------------------------------------------------------------------------------- |
| `type` | String | Y | Type of MCP server connection, must be `"remote"`. |
| `url` | String | Y | URL of the remote MCP server. |
| `enabled` | Boolean | | Enable or disable the MCP server on startup. |
| `headers` | Object | | Headers to send with the request. |
| `oauth` | Object | | OAuth authentication configuration. See [OAuth](#oauth) section below. |
| `timeout` | Number | | Timeout in ms for fetching tools from the MCP server. Defaults to 5000 (5 seconds). |
| Option | Type | Required | Description |
| ---------- | ------- | -------- | ---------------------------------------------------------------------------------------------------- |
| `type` | String | Y | Type of MCP server connection, must be `"remote"`. |
| `url` | String | Y | URL of the remote MCP server. |
| `enabled` | Boolean | | Enable or disable the MCP server on startup. |
| `headers` | Object | | Headers to send with the request. |
| `oauth` | Object | | OAuth authentication configuration. See [OAuth](#oauth) section below. |
| `timeout` | Number | | Timeout in ms for fetching tools from the MCP server. Defaults to 5000 (5 seconds). |
| `insecure` | Boolean | | Disable TLS certificate verification. Only use for self-signed certificates in trusted environments. |

---

Expand Down Expand Up @@ -264,6 +265,33 @@ If you want to disable automatic OAuth for a server (e.g., for servers that use

---

#### Self-signed certificates

If your remote MCP server uses a self-signed or internal TLS certificate, connections will fail with a certificate verification error. Set `insecure` to `true` to skip verification.

:::caution
Only use `insecure` for servers in trusted environments. This disables all TLS certificate validation for that server.
:::

```json title="opencode.json" {7}
{
"$schema": "https://opencode.ai/config.json",
"mcp": {
"internal-server": {
"type": "remote",
"url": "https://10.0.0.5:8443/mcp",
"insecure": true,
"oauth": false,
"headers": {
"Authorization": "Bearer {env:MY_TOKEN}"
}
}
}
}
```

---

#### OAuth Options

| Option | Type | Description |
Expand Down
Loading