feat(httpapi): bridge mcp oauth endpoints (#24405)

This commit is contained in:
Kit Langton
2026-04-25 19:27:11 -04:00
committed by GitHub
parent 3e35c974a4
commit 450128f9be
4 changed files with 121 additions and 9 deletions

View File

@@ -1,7 +1,7 @@
import { MCP } from "@/mcp"
import { ConfigMCP } from "@/config/mcp"
import { Effect, Layer, Schema } from "effect"
import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
import { Authorization } from "./auth"
const AddPayload = Schema.Struct({
@@ -10,9 +10,22 @@ const AddPayload = Schema.Struct({
}).annotate({ identifier: "McpAddInput" })
const StatusMap = Schema.Record(Schema.String, MCP.Status)
const AuthStartResponse = Schema.Struct({
authorizationUrl: Schema.String,
oauthState: Schema.String,
}).annotate({ identifier: "McpAuthStartResponse" })
const AuthCallbackPayload = Schema.Struct({
code: Schema.String,
}).annotate({ identifier: "McpAuthCallbackInput" })
const AuthRemoveResponse = Schema.Struct({
success: Schema.Literal(true),
}).annotate({ identifier: "McpAuthRemoveResponse" })
export const McpPaths = {
status: "/mcp",
auth: "/mcp/:name/auth",
authCallback: "/mcp/:name/auth/callback",
authAuthenticate: "/mcp/:name/auth/authenticate",
connect: "/mcp/:name/connect",
disconnect: "/mcp/:name/disconnect",
} as const
@@ -40,6 +53,47 @@ export const McpApi = HttpApi.make("mcp")
description: "Dynamically add a new Model Context Protocol (MCP) server to the system.",
}),
),
HttpApiEndpoint.post("authStart", McpPaths.auth, {
params: { name: Schema.String },
success: AuthStartResponse,
}).annotateMerge(
OpenApi.annotations({
identifier: "mcp.auth.start",
summary: "Start MCP OAuth",
description: "Start OAuth authentication flow for a Model Context Protocol (MCP) server.",
}),
),
HttpApiEndpoint.post("authCallback", McpPaths.authCallback, {
params: { name: Schema.String },
payload: AuthCallbackPayload,
success: MCP.Status,
}).annotateMerge(
OpenApi.annotations({
identifier: "mcp.auth.callback",
summary: "Complete MCP OAuth",
description: "Complete OAuth authentication for a Model Context Protocol (MCP) server using the authorization code.",
}),
),
HttpApiEndpoint.post("authAuthenticate", McpPaths.authAuthenticate, {
params: { name: Schema.String },
success: MCP.Status,
}).annotateMerge(
OpenApi.annotations({
identifier: "mcp.auth.authenticate",
summary: "Authenticate MCP OAuth",
description: "Start OAuth flow and wait for callback (opens browser).",
}),
),
HttpApiEndpoint.delete("authRemove", McpPaths.auth, {
params: { name: Schema.String },
success: AuthRemoveResponse,
}).annotateMerge(
OpenApi.annotations({
identifier: "mcp.auth.remove",
summary: "Remove MCP OAuth",
description: "Remove OAuth credentials for an MCP server.",
}),
),
HttpApiEndpoint.post("connect", McpPaths.connect, {
params: { name: Schema.String },
success: Schema.Boolean,
@@ -89,6 +143,28 @@ export const mcpHandlers = Layer.unwrap(
return Schema.decodeUnknownSync(StatusMap)("status" in result ? { [payload.name]: result } : result)
})
const authStart = Effect.fn("McpHttpApi.authStart")(function* (ctx: { params: { name: string } }) {
if (!(yield* mcp.supportsOAuth(ctx.params.name))) return yield* new HttpApiError.BadRequest({})
return yield* mcp.startAuth(ctx.params.name)
})
const authCallback = Effect.fn("McpHttpApi.authCallback")(function* (ctx: {
params: { name: string }
payload: typeof AuthCallbackPayload.Type
}) {
return yield* mcp.finishAuth(ctx.params.name, ctx.payload.code)
})
const authAuthenticate = Effect.fn("McpHttpApi.authAuthenticate")(function* (ctx: { params: { name: string } }) {
if (!(yield* mcp.supportsOAuth(ctx.params.name))) return yield* new HttpApiError.BadRequest({})
return yield* mcp.authenticate(ctx.params.name)
})
const authRemove = Effect.fn("McpHttpApi.authRemove")(function* (ctx: { params: { name: string } }) {
yield* mcp.removeAuth(ctx.params.name)
return { success: true as const }
})
const connect = Effect.fn("McpHttpApi.connect")(function* (ctx: { params: { name: string } }) {
yield* mcp.connect(ctx.params.name)
return true
@@ -100,7 +176,15 @@ export const mcpHandlers = Layer.unwrap(
})
return HttpApiBuilder.group(McpApi, "mcp", (handlers) =>
handlers.handle("status", status).handle("add", add).handle("connect", connect).handle("disconnect", disconnect),
handlers
.handle("status", status)
.handle("add", add)
.handle("authStart", authStart)
.handle("authCallback", authCallback)
.handle("authAuthenticate", authAuthenticate)
.handle("authRemove", authRemove)
.handle("connect", connect)
.handle("disconnect", disconnect),
)
}),
).pipe(Layer.provide(MCP.defaultLayer))

View File

@@ -80,6 +80,10 @@ export const InstanceRoutes = (upgrade: UpgradeWebSocket): Hono => {
app.get(InstancePaths.formatter, (c) => handler(c.req.raw, context))
app.get(McpPaths.status, (c) => handler(c.req.raw, context))
app.post(McpPaths.status, (c) => handler(c.req.raw, context))
app.post(McpPaths.auth, (c) => handler(c.req.raw, context))
app.post(McpPaths.authCallback, (c) => handler(c.req.raw, context))
app.post(McpPaths.authAuthenticate, (c) => handler(c.req.raw, context))
app.delete(McpPaths.auth, (c) => handler(c.req.raw, context))
app.post(McpPaths.connect, (c) => handler(c.req.raw, context))
app.post(McpPaths.disconnect, (c) => handler(c.req.raw, context))
}