Skip to content

Commit fe9a936

Browse files
authored
feat(server): expose runtime MCP controls over HTTP (anomalyco#37712)
1 parent d5669ca commit fe9a936

11 files changed

Lines changed: 473 additions & 131 deletions

File tree

packages/client/src/effect/api/api.ts

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -542,13 +542,50 @@ export type Endpoint11_0Input = { readonly location?: Endpoint11_0Request["query
542542
export type Endpoint11_0Output = EffectValue<ReturnType<RawClient["server.mcp"]["mcp.list"]>>
543543
export type McpListOperation<E = never> = (input?: Endpoint11_0Input) => Effect.Effect<Endpoint11_0Output, E>
544544

545-
type Endpoint11_1Request = Parameters<RawClient["server.mcp"]["mcp.resource.catalog"]>[0]
546-
export type Endpoint11_1Input = { readonly location?: Endpoint11_1Request["query"]["location"] }
547-
export type Endpoint11_1Output = EffectValue<ReturnType<RawClient["server.mcp"]["mcp.resource.catalog"]>>
548-
export type McpResourceCatalogOperation<E = never> = (input?: Endpoint11_1Input) => Effect.Effect<Endpoint11_1Output, E>
545+
type Endpoint11_1Request = Parameters<RawClient["server.mcp"]["mcp.add"]>[0]
546+
export type Endpoint11_1Input = {
547+
readonly server: Endpoint11_1Request["params"]["server"]
548+
readonly location?: Endpoint11_1Request["query"]["location"]
549+
readonly config: Endpoint11_1Request["payload"]["config"]
550+
}
551+
export type Endpoint11_1Output = EffectValue<ReturnType<RawClient["server.mcp"]["mcp.add"]>>
552+
export type McpAddOperation<E = never> = (input: Endpoint11_1Input) => Effect.Effect<Endpoint11_1Output, E>
553+
554+
type Endpoint11_2Request = Parameters<RawClient["server.mcp"]["mcp.remove"]>[0]
555+
export type Endpoint11_2Input = {
556+
readonly server: Endpoint11_2Request["params"]["server"]
557+
readonly location?: Endpoint11_2Request["query"]["location"]
558+
}
559+
export type Endpoint11_2Output = EffectValue<ReturnType<RawClient["server.mcp"]["mcp.remove"]>>
560+
export type McpRemoveOperation<E = never> = (input: Endpoint11_2Input) => Effect.Effect<Endpoint11_2Output, E>
561+
562+
type Endpoint11_3Request = Parameters<RawClient["server.mcp"]["mcp.connect"]>[0]
563+
export type Endpoint11_3Input = {
564+
readonly server: Endpoint11_3Request["params"]["server"]
565+
readonly location?: Endpoint11_3Request["query"]["location"]
566+
}
567+
export type Endpoint11_3Output = EffectValue<ReturnType<RawClient["server.mcp"]["mcp.connect"]>>
568+
export type McpConnectOperation<E = never> = (input: Endpoint11_3Input) => Effect.Effect<Endpoint11_3Output, E>
569+
570+
type Endpoint11_4Request = Parameters<RawClient["server.mcp"]["mcp.disconnect"]>[0]
571+
export type Endpoint11_4Input = {
572+
readonly server: Endpoint11_4Request["params"]["server"]
573+
readonly location?: Endpoint11_4Request["query"]["location"]
574+
}
575+
export type Endpoint11_4Output = EffectValue<ReturnType<RawClient["server.mcp"]["mcp.disconnect"]>>
576+
export type McpDisconnectOperation<E = never> = (input: Endpoint11_4Input) => Effect.Effect<Endpoint11_4Output, E>
577+
578+
type Endpoint11_5Request = Parameters<RawClient["server.mcp"]["mcp.resource.catalog"]>[0]
579+
export type Endpoint11_5Input = { readonly location?: Endpoint11_5Request["query"]["location"] }
580+
export type Endpoint11_5Output = EffectValue<ReturnType<RawClient["server.mcp"]["mcp.resource.catalog"]>>
581+
export type McpResourceCatalogOperation<E = never> = (input?: Endpoint11_5Input) => Effect.Effect<Endpoint11_5Output, E>
549582

550583
export interface McpApi<E = never> {
551584
readonly list: McpListOperation<E>
585+
readonly add: McpAddOperation<E>
586+
readonly remove: McpRemoveOperation<E>
587+
readonly connect: McpConnectOperation<E>
588+
readonly disconnect: McpDisconnectOperation<E>
552589
readonly resource: { readonly catalog: McpResourceCatalogOperation<E> }
553590
}
554591

packages/client/src/effect/generated/client.ts

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -650,14 +650,61 @@ type Endpoint11_0Input = { readonly location?: Endpoint11_0Request["query"]["loc
650650
const Endpoint11_0 = (raw: RawClient["server.mcp"]) => (input?: Endpoint11_0Input) =>
651651
raw["mcp.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError))
652652

653-
type Endpoint11_1Request = Parameters<RawClient["server.mcp"]["mcp.resource.catalog"]>[0]
654-
type Endpoint11_1Input = { readonly location?: Endpoint11_1Request["query"]["location"] }
655-
const Endpoint11_1 = (raw: RawClient["server.mcp"]) => (input?: Endpoint11_1Input) =>
653+
type Endpoint11_1Request = Parameters<RawClient["server.mcp"]["mcp.add"]>[0]
654+
type Endpoint11_1Input = {
655+
readonly server: Endpoint11_1Request["params"]["server"]
656+
readonly location?: Endpoint11_1Request["query"]["location"]
657+
readonly config: Endpoint11_1Request["payload"]["config"]
658+
}
659+
const Endpoint11_1 = (raw: RawClient["server.mcp"]) => (input: Endpoint11_1Input) =>
660+
raw["mcp.add"]({
661+
params: { server: input["server"] },
662+
query: { location: input["location"] },
663+
payload: { config: input["config"] },
664+
}).pipe(Effect.mapError(mapClientError))
665+
666+
type Endpoint11_2Request = Parameters<RawClient["server.mcp"]["mcp.remove"]>[0]
667+
type Endpoint11_2Input = {
668+
readonly server: Endpoint11_2Request["params"]["server"]
669+
readonly location?: Endpoint11_2Request["query"]["location"]
670+
}
671+
const Endpoint11_2 = (raw: RawClient["server.mcp"]) => (input: Endpoint11_2Input) =>
672+
raw["mcp.remove"]({ params: { server: input["server"] }, query: { location: input["location"] } }).pipe(
673+
Effect.mapError(mapClientError),
674+
)
675+
676+
type Endpoint11_3Request = Parameters<RawClient["server.mcp"]["mcp.connect"]>[0]
677+
type Endpoint11_3Input = {
678+
readonly server: Endpoint11_3Request["params"]["server"]
679+
readonly location?: Endpoint11_3Request["query"]["location"]
680+
}
681+
const Endpoint11_3 = (raw: RawClient["server.mcp"]) => (input: Endpoint11_3Input) =>
682+
raw["mcp.connect"]({ params: { server: input["server"] }, query: { location: input["location"] } }).pipe(
683+
Effect.mapError(mapClientError),
684+
)
685+
686+
type Endpoint11_4Request = Parameters<RawClient["server.mcp"]["mcp.disconnect"]>[0]
687+
type Endpoint11_4Input = {
688+
readonly server: Endpoint11_4Request["params"]["server"]
689+
readonly location?: Endpoint11_4Request["query"]["location"]
690+
}
691+
const Endpoint11_4 = (raw: RawClient["server.mcp"]) => (input: Endpoint11_4Input) =>
692+
raw["mcp.disconnect"]({ params: { server: input["server"] }, query: { location: input["location"] } }).pipe(
693+
Effect.mapError(mapClientError),
694+
)
695+
696+
type Endpoint11_5Request = Parameters<RawClient["server.mcp"]["mcp.resource.catalog"]>[0]
697+
type Endpoint11_5Input = { readonly location?: Endpoint11_5Request["query"]["location"] }
698+
const Endpoint11_5 = (raw: RawClient["server.mcp"]) => (input?: Endpoint11_5Input) =>
656699
raw["mcp.resource.catalog"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError))
657700

658701
const adaptGroup11 = (raw: RawClient["server.mcp"]) => ({
659702
list: Endpoint11_0(raw),
660-
resource: { catalog: Endpoint11_1(raw) },
703+
add: Endpoint11_1(raw),
704+
remove: Endpoint11_2(raw),
705+
connect: Endpoint11_3(raw),
706+
disconnect: Endpoint11_4(raw),
707+
resource: { catalog: Endpoint11_5(raw) },
661708
})
662709

663710
type Endpoint12_0Request = Parameters<RawClient["server.credential"]["credential.update"]>[0]

packages/client/src/promise/generated/client.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,14 @@ import type {
104104
IntegrationCommandCancelOutput,
105105
McpListInput,
106106
McpListOutput,
107+
McpAddInput,
108+
McpAddOutput,
109+
McpRemoveInput,
110+
McpRemoveOutput,
111+
McpConnectInput,
112+
McpConnectOutput,
113+
McpDisconnectInput,
114+
McpDisconnectOutput,
107115
McpResourceCatalogInput,
108116
McpResourceCatalogOutput,
109117
CredentialUpdateInput,
@@ -1044,6 +1052,55 @@ export function make(options: ClientOptions) {
10441052
},
10451053
requestOptions,
10461054
),
1055+
add: (input: McpAddInput, requestOptions?: RequestOptions) =>
1056+
request<McpAddOutput>(
1057+
{
1058+
method: "PUT",
1059+
path: `/api/mcp/${encodeURIComponent(input.server)}`,
1060+
query: { location: input["location"] },
1061+
body: { config: input["config"] },
1062+
successStatus: 204,
1063+
declaredStatuses: [401, 400],
1064+
empty: true,
1065+
},
1066+
requestOptions,
1067+
),
1068+
remove: (input: McpRemoveInput, requestOptions?: RequestOptions) =>
1069+
request<McpRemoveOutput>(
1070+
{
1071+
method: "DELETE",
1072+
path: `/api/mcp/${encodeURIComponent(input.server)}`,
1073+
query: { location: input["location"] },
1074+
successStatus: 204,
1075+
declaredStatuses: [404, 401, 400],
1076+
empty: true,
1077+
},
1078+
requestOptions,
1079+
),
1080+
connect: (input: McpConnectInput, requestOptions?: RequestOptions) =>
1081+
request<McpConnectOutput>(
1082+
{
1083+
method: "POST",
1084+
path: `/api/mcp/${encodeURIComponent(input.server)}/connect`,
1085+
query: { location: input["location"] },
1086+
successStatus: 204,
1087+
declaredStatuses: [404, 401, 400],
1088+
empty: true,
1089+
},
1090+
requestOptions,
1091+
),
1092+
disconnect: (input: McpDisconnectInput, requestOptions?: RequestOptions) =>
1093+
request<McpDisconnectOutput>(
1094+
{
1095+
method: "POST",
1096+
path: `/api/mcp/${encodeURIComponent(input.server)}/disconnect`,
1097+
query: { location: input["location"] },
1098+
successStatus: 204,
1099+
declaredStatuses: [404, 401, 400],
1100+
empty: true,
1101+
},
1102+
requestOptions,
1103+
),
10471104
resource: {
10481105
catalog: (input?: McpResourceCatalogInput, requestOptions?: RequestOptions) =>
10491106
request<McpResourceCatalogOutput>(

packages/client/src/promise/generated/types.ts

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2482,6 +2482,14 @@ export type ProviderNotFoundError = {
24822482
export const isProviderNotFoundError = (value: unknown): value is ProviderNotFoundError =>
24832483
typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "ProviderNotFoundError"
24842484

2485+
export type McpServerNotFoundError = {
2486+
readonly _tag: "McpServerNotFoundError"
2487+
readonly server: string
2488+
readonly message: string
2489+
}
2490+
export const isMcpServerNotFoundError = (value: unknown): value is McpServerNotFoundError =>
2491+
typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "McpServerNotFoundError"
2492+
24852493
export type FormNotFoundError = { readonly _tag: "FormNotFoundError"; readonly id: string; readonly message: string }
24862494
export const isFormNotFoundError = (value: unknown): value is FormNotFoundError =>
24872495
typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "FormNotFoundError"
@@ -3461,6 +3469,84 @@ export type McpListOutput = {
34613469
data: Array<McpServer>
34623470
}
34633471

3472+
export type McpAddInput = {
3473+
readonly server: { readonly server: string }["server"]
3474+
readonly location?: {
3475+
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
3476+
}["location"]
3477+
readonly config: {
3478+
readonly config:
3479+
| {
3480+
readonly type: "local"
3481+
readonly command: ReadonlyArray<string>
3482+
readonly cwd?: string | undefined
3483+
readonly environment?: { readonly [x: string]: string } | undefined
3484+
readonly disabled?: boolean | undefined
3485+
readonly codemode?: boolean | undefined
3486+
readonly timeout?:
3487+
| {
3488+
readonly startup?: number | undefined
3489+
readonly catalog?: number | undefined
3490+
readonly execution?: number | undefined
3491+
}
3492+
| undefined
3493+
}
3494+
| {
3495+
readonly type: "remote"
3496+
readonly url: string
3497+
readonly headers?: { readonly [x: string]: string } | undefined
3498+
readonly oauth?:
3499+
| {
3500+
readonly client_id?: string | undefined
3501+
readonly client_secret?: string | undefined
3502+
readonly scope?: string | undefined
3503+
readonly callback_port?: number | undefined
3504+
readonly redirect_uri?: string | undefined
3505+
}
3506+
| false
3507+
| undefined
3508+
readonly disabled?: boolean | undefined
3509+
readonly codemode?: boolean | undefined
3510+
readonly timeout?:
3511+
| {
3512+
readonly startup?: number | undefined
3513+
readonly catalog?: number | undefined
3514+
readonly execution?: number | undefined
3515+
}
3516+
| undefined
3517+
}
3518+
}["config"]
3519+
}
3520+
3521+
export type McpAddOutput = void
3522+
3523+
export type McpRemoveInput = {
3524+
readonly server: { readonly server: string }["server"]
3525+
readonly location?: {
3526+
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
3527+
}["location"]
3528+
}
3529+
3530+
export type McpRemoveOutput = void
3531+
3532+
export type McpConnectInput = {
3533+
readonly server: { readonly server: string }["server"]
3534+
readonly location?: {
3535+
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
3536+
}["location"]
3537+
}
3538+
3539+
export type McpConnectOutput = void
3540+
3541+
export type McpDisconnectInput = {
3542+
readonly server: { readonly server: string }["server"]
3543+
readonly location?: {
3544+
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
3545+
}["location"]
3546+
}
3547+
3548+
export type McpDisconnectOutput = void
3549+
34643550
export type McpResourceCatalogInput = {
34653551
readonly location?: {
34663552
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined

packages/core/src/config/mcp.ts

Lines changed: 12 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,19 @@
11
export * as ConfigMCP from "./mcp"
22

33
import { Schema } from "effect"
4-
import { PositiveInt } from "../schema"
4+
import { Mcp } from "@opencode-ai/schema/mcp"
55

6-
export class Timeout extends Schema.Class<Timeout>("ConfigV2.MCP.Timeout")({
7-
startup: PositiveInt.pipe(Schema.optional).annotate({
8-
description: "Maximum time in milliseconds to establish and initialize the MCP server.",
9-
}),
10-
catalog: PositiveInt.pipe(Schema.optional).annotate({
11-
description: "Maximum time in milliseconds to wait for MCP discovery requests such as tools/list and prompts/list.",
12-
}),
13-
execution: PositiveInt.pipe(Schema.optional).annotate({
14-
description: "Maximum time in milliseconds to wait for MCP tool and prompt execution.",
15-
}),
16-
}) {}
17-
18-
export class Local extends Schema.Class<Local>("ConfigV2.MCP.Local")({
19-
type: Schema.Literal("local"),
20-
command: Schema.String.pipe(Schema.Array),
21-
cwd: Schema.String.pipe(Schema.optional).annotate({
22-
description: "Working directory for the MCP server process. Relative paths resolve from the workspace directory.",
23-
}),
24-
environment: Schema.Record(Schema.String, Schema.String).pipe(Schema.optional),
25-
disabled: Schema.Boolean.pipe(Schema.optional),
26-
codemode: Schema.Boolean.pipe(Schema.optional).annotate({
27-
description: "Expose this server's tools through Code Mode. Defaults to true.",
28-
}),
29-
timeout: Timeout.pipe(Schema.optional),
30-
}) {}
31-
32-
export class OAuth extends Schema.Class<OAuth>("ConfigV2.MCP.OAuth")({
33-
client_id: Schema.String.pipe(Schema.optional),
34-
client_secret: Schema.String.pipe(Schema.optional),
35-
scope: Schema.String.pipe(Schema.optional),
36-
callback_port: Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 65535 })).pipe(Schema.optional),
37-
redirect_uri: Schema.String.pipe(Schema.optional),
38-
}) {}
39-
40-
export class Remote extends Schema.Class<Remote>("ConfigV2.MCP.Remote")({
41-
type: Schema.Literal("remote"),
42-
url: Schema.String,
43-
headers: Schema.Record(Schema.String, Schema.String).pipe(Schema.optional),
44-
oauth: Schema.Union([OAuth, Schema.Literal(false)]).pipe(Schema.optional),
45-
disabled: Schema.Boolean.pipe(Schema.optional),
46-
codemode: Schema.Boolean.pipe(Schema.optional).annotate({
47-
description: "Expose this server's tools through Code Mode. Defaults to true.",
48-
}),
49-
timeout: Timeout.pipe(Schema.optional),
50-
}) {}
51-
52-
export const Server = Schema.Union([Local, Remote]).pipe(Schema.toTaggedUnion("type"))
6+
// The MCP server config is a public wire contract (used by the mcp.add route), so it lives in
7+
// @opencode-ai/schema and is re-exported here.
8+
export const Timeout = Mcp.TimeoutConfig
9+
export type Timeout = Mcp.TimeoutConfig
10+
export const Local = Mcp.LocalConfig
11+
export type Local = Mcp.LocalConfig
12+
export const OAuth = Mcp.OAuthConfig
13+
export type OAuth = Mcp.OAuthConfig
14+
export const Remote = Mcp.RemoteConfig
15+
export type Remote = Mcp.RemoteConfig
16+
export const Server = Mcp.ServerConfig
5317

5418
export class Info extends Schema.Class<Info>("ConfigV2.MCP")({
5519
timeout: Timeout.pipe(Schema.optional),

packages/protocol/src/errors.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,15 @@ export class SkillNotFoundError extends Schema.TaggedErrorClass<SkillNotFoundErr
9090
{ httpApiStatus: 404 },
9191
) {}
9292

93+
export class McpServerNotFoundError extends Schema.TaggedErrorClass<McpServerNotFoundError>()(
94+
"McpServerNotFoundError",
95+
{
96+
server: Schema.String,
97+
message: Schema.String,
98+
},
99+
{ httpApiStatus: 404 },
100+
) {}
101+
93102
export class CommandNotFoundError extends Schema.TaggedErrorClass<CommandNotFoundError>()(
94103
"CommandNotFoundError",
95104
{

0 commit comments

Comments
 (0)