fix(httpapi): return mcp server not found errors (#28817)

This commit is contained in:
Shoubhit Dash
2026-05-22 17:46:30 +05:30
committed by GitHub
parent 51da3483a9
commit 0beb4de3e8
8 changed files with 158 additions and 72 deletions

View File

@@ -67,6 +67,10 @@ export const Failed = NamedError.create("MCPFailed", {
name: Schema.String,
})
export class NotFoundError extends Schema.TaggedErrorClass<NotFoundError>()("MCP.NotFoundError", {
name: Schema.String,
}) {}
type MCPClient = Client
const StatusConnected = Schema.Struct({ status: Schema.Literal("connected") }).annotate({
@@ -242,8 +246,8 @@ export interface Interface {
readonly prompts: () => Effect.Effect<Record<string, PromptInfo & { client: string }>>
readonly resources: () => Effect.Effect<Record<string, ResourceInfo & { client: string }>>
readonly add: (name: string, mcp: ConfigMCP.Info) => Effect.Effect<{ status: Record<string, Status> | Status }>
readonly connect: (name: string) => Effect.Effect<void>
readonly disconnect: (name: string) => Effect.Effect<void>
readonly connect: (name: string) => Effect.Effect<void, NotFoundError>
readonly disconnect: (name: string) => Effect.Effect<void, NotFoundError>
readonly getPrompt: (
clientName: string,
name: string,
@@ -253,11 +257,11 @@ export interface Interface {
clientName: string,
resourceUri: string,
) => Effect.Effect<Awaited<ReturnType<MCPClient["readResource"]>> | undefined>
readonly startAuth: (mcpName: string) => Effect.Effect<{ authorizationUrl: string; oauthState: string }>
readonly authenticate: (mcpName: string) => Effect.Effect<Status>
readonly finishAuth: (mcpName: string, authorizationCode: string) => Effect.Effect<Status>
readonly startAuth: (mcpName: string) => Effect.Effect<{ authorizationUrl: string; oauthState: string }, NotFoundError>
readonly authenticate: (mcpName: string) => Effect.Effect<Status, NotFoundError>
readonly finishAuth: (mcpName: string, authorizationCode: string) => Effect.Effect<Status, NotFoundError>
readonly removeAuth: (mcpName: string) => Effect.Effect<void>
readonly supportsOAuth: (mcpName: string) => Effect.Effect<boolean>
readonly supportsOAuth: (mcpName: string) => Effect.Effect<boolean, NotFoundError>
readonly hasStoredTokens: (mcpName: string) => Effect.Effect<boolean>
readonly getAuthStatus: (mcpName: string) => Effect.Effect<AuthStatus>
}
@@ -642,15 +646,12 @@ export const layer = Layer.effect(
})
const connect = Effect.fn("MCP.connect")(function* (name: string) {
const mcp = yield* getMcpConfig(name)
if (!mcp) {
log.error("MCP config not found or invalid", { name })
return
}
const mcp = yield* requireMcpConfig(name)
yield* createAndStore(name, { ...mcp, enabled: true })
})
const disconnect = Effect.fn("MCP.disconnect")(function* (name: string) {
yield* requireMcpConfig(name)
const s = yield* InstanceState.get(state)
yield* closeClient(s, name)
delete s.clients[name]
@@ -759,9 +760,14 @@ export const layer = Layer.effect(
return mcpConfig
})
const startAuth = Effect.fn("MCP.startAuth")(function* (mcpName: string) {
const requireMcpConfig = Effect.fnUntraced(function* (mcpName: string) {
const mcpConfig = yield* getMcpConfig(mcpName)
if (!mcpConfig) throw new Error(`MCP server ${mcpName} not found or disabled`)
if (!mcpConfig) return yield* new NotFoundError({ name: mcpName })
return mcpConfig
})
const startAuth = Effect.fn("MCP.startAuth")(function* (mcpName: string) {
const mcpConfig = yield* requireMcpConfig(mcpName)
if (mcpConfig.type !== "remote") throw new Error(`MCP server ${mcpName} is not a remote server`)
if (mcpConfig.oauth === false) throw new Error(`MCP server ${mcpName} has OAuth explicitly disabled`)
const url = remoteURL(mcpName, mcpConfig.url)
@@ -825,11 +831,9 @@ export const layer = Layer.effect(
const result = yield* startAuth(mcpName)
if (!result.authorizationUrl) {
const client = "client" in result ? result.client : undefined
const mcpConfig = yield* getMcpConfig(mcpName)
if (!mcpConfig) {
yield* Effect.tryPromise(() => client?.close() ?? Promise.resolve()).pipe(Effect.ignore)
return { status: "failed", error: "MCP config not found after auth" } as Status
}
const mcpConfig = yield* requireMcpConfig(mcpName).pipe(
Effect.tapError(() => Effect.tryPromise(() => client?.close() ?? Promise.resolve()).pipe(Effect.ignore)),
)
const listed = client ? yield* defs(mcpName, client, mcpConfig.timeout) : undefined
if (!client || !listed) {
@@ -880,6 +884,7 @@ export const layer = Layer.effect(
})
const finishAuth = Effect.fn("MCP.finishAuth")(function* (mcpName: string, authorizationCode: string) {
yield* requireMcpConfig(mcpName)
const transport = pendingOAuthTransports.get(mcpName)
if (!transport) throw new Error(`No pending OAuth flow for MCP server: ${mcpName}`)
@@ -898,8 +903,7 @@ export const layer = Layer.effect(
yield* auth.clearCodeVerifier(mcpName)
pendingOAuthTransports.delete(mcpName)
const mcpConfig = yield* getMcpConfig(mcpName)
if (!mcpConfig) return { status: "failed", error: "MCP config not found after auth" } as Status
const mcpConfig = yield* requireMcpConfig(mcpName)
return yield* createAndStore(mcpName, mcpConfig)
})
@@ -912,8 +916,7 @@ export const layer = Layer.effect(
})
const supportsOAuth = Effect.fn("MCP.supportsOAuth")(function* (mcpName: string) {
const mcpConfig = yield* getMcpConfig(mcpName)
if (!mcpConfig) return false
const mcpConfig = yield* requireMcpConfig(mcpName)
return mcpConfig.type === "remote" && mcpConfig.oauth !== false
})

View File

@@ -140,6 +140,15 @@ export class PermissionNotFoundError extends Schema.TaggedErrorClass<PermissionN
{ httpApiStatus: 404 },
) {}
export class McpServerNotFoundError extends Schema.TaggedErrorClass<McpServerNotFoundError>()(
"McpServerNotFoundError",
{
name: Schema.String,
message: Schema.String,
},
{ httpApiStatus: 404 },
) {}
export class ApiNotFoundError extends Schema.ErrorClass<ApiNotFoundError>("NotFoundError")(
{
name: Schema.Literal("NotFoundError"),

View File

@@ -2,6 +2,7 @@ import { MCP } from "@/mcp"
import { ConfigMCP } from "@/config/mcp"
import { Schema } from "effect"
import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
import { McpServerNotFoundError } from "../errors"
import { Authorization } from "../middleware/authorization"
import { InstanceContextMiddleware } from "../middleware/instance-context"
import { WorkspaceRoutingMiddleware, WorkspaceRoutingQuery } from "../middleware/workspace-routing"
@@ -67,7 +68,7 @@ export const McpApi = HttpApi.make("mcp")
params: { name: Schema.String },
query: WorkspaceRoutingQuery,
success: described(AuthStartResponse, "OAuth flow started"),
error: [UnsupportedOAuthError, HttpApiError.NotFound],
error: [UnsupportedOAuthError, McpServerNotFoundError],
}).annotateMerge(
OpenApi.annotations({
identifier: "mcp.auth.start",
@@ -80,7 +81,7 @@ export const McpApi = HttpApi.make("mcp")
query: WorkspaceRoutingQuery,
payload: AuthCallbackPayload,
success: described(MCP.Status, "OAuth authentication completed"),
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
error: [HttpApiError.BadRequest, McpServerNotFoundError],
}).annotateMerge(
OpenApi.annotations({
identifier: "mcp.auth.callback",
@@ -93,7 +94,7 @@ export const McpApi = HttpApi.make("mcp")
params: { name: Schema.String },
query: WorkspaceRoutingQuery,
success: described(MCP.Status, "OAuth authentication completed"),
error: [UnsupportedOAuthError, HttpApiError.NotFound],
error: [UnsupportedOAuthError, McpServerNotFoundError],
}).annotateMerge(
OpenApi.annotations({
identifier: "mcp.auth.authenticate",
@@ -105,7 +106,7 @@ export const McpApi = HttpApi.make("mcp")
params: { name: Schema.String },
query: WorkspaceRoutingQuery,
success: described(AuthRemoveResponse, "OAuth credentials removed"),
error: HttpApiError.NotFound,
error: McpServerNotFoundError,
}).annotateMerge(
OpenApi.annotations({
identifier: "mcp.auth.remove",
@@ -117,6 +118,7 @@ export const McpApi = HttpApi.make("mcp")
params: { name: Schema.String },
query: WorkspaceRoutingQuery,
success: described(Schema.Boolean, "MCP server connected successfully"),
error: McpServerNotFoundError,
}).annotateMerge(
OpenApi.annotations({
identifier: "mcp.connect",
@@ -127,6 +129,7 @@ export const McpApi = HttpApi.make("mcp")
params: { name: Schema.String },
query: WorkspaceRoutingQuery,
success: described(Schema.Boolean, "MCP server disconnected successfully"),
error: McpServerNotFoundError,
}).annotateMerge(
OpenApi.annotations({
identifier: "mcp.disconnect",

View File

@@ -2,6 +2,7 @@ import { MCP } from "@/mcp"
import { Effect, Schema } from "effect"
import { HttpApiBuilder, HttpApiError } from "effect/unstable/httpapi"
import { InstanceHttpApi } from "../api"
import { McpServerNotFoundError } from "../errors"
import { AddPayload, AuthCallbackPayload, StatusMap, UnsupportedOAuthError } from "../groups/mcp"
export const mcpHandlers = HttpApiBuilder.group(InstanceHttpApi, "mcp", (handlers) =>
@@ -20,38 +21,74 @@ export const mcpHandlers = HttpApiBuilder.group(InstanceHttpApi, "mcp", (handler
})
const authStart = Effect.fn("McpHttpApi.authStart")(function* (ctx: { params: { name: string } }) {
if (!(yield* mcp.supportsOAuth(ctx.params.name))) {
return yield* new UnsupportedOAuthError({ error: `MCP server ${ctx.params.name} does not support OAuth` })
}
return yield* mcp.startAuth(ctx.params.name)
return yield* Effect.gen(function* () {
if (!(yield* mcp.supportsOAuth(ctx.params.name))) {
return yield* new UnsupportedOAuthError({ error: `MCP server ${ctx.params.name} does not support OAuth` })
}
return yield* mcp.startAuth(ctx.params.name)
}).pipe(
Effect.catchTag("MCP.NotFoundError", (error) =>
Effect.fail(new McpServerNotFoundError({ name: error.name, message: `MCP server not found: ${error.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)
return yield* mcp
.finishAuth(ctx.params.name, ctx.payload.code)
.pipe(
Effect.catchTag("MCP.NotFoundError", (error) =>
Effect.fail(new McpServerNotFoundError({ name: error.name, message: `MCP server not found: ${error.name}` })),
),
)
})
const authAuthenticate = Effect.fn("McpHttpApi.authAuthenticate")(function* (ctx: { params: { name: string } }) {
if (!(yield* mcp.supportsOAuth(ctx.params.name))) {
return yield* new UnsupportedOAuthError({ error: `MCP server ${ctx.params.name} does not support OAuth` })
}
return yield* mcp.authenticate(ctx.params.name)
return yield* Effect.gen(function* () {
if (!(yield* mcp.supportsOAuth(ctx.params.name))) {
return yield* new UnsupportedOAuthError({ error: `MCP server ${ctx.params.name} does not support OAuth` })
}
return yield* mcp.authenticate(ctx.params.name)
}).pipe(
Effect.catchTag("MCP.NotFoundError", (error) =>
Effect.fail(new McpServerNotFoundError({ name: error.name, message: `MCP server not found: ${error.name}` })),
),
)
})
const authRemove = Effect.fn("McpHttpApi.authRemove")(function* (ctx: { params: { name: string } }) {
const status = yield* mcp.status()
if (!(ctx.params.name in status))
return yield* new McpServerNotFoundError({
name: ctx.params.name,
message: `MCP server not found: ${ctx.params.name}`,
})
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)
yield* mcp
.connect(ctx.params.name)
.pipe(
Effect.catchTag("MCP.NotFoundError", (error) =>
Effect.fail(new McpServerNotFoundError({ name: error.name, message: `MCP server not found: ${error.name}` })),
),
)
return true
})
const disconnect = Effect.fn("McpHttpApi.disconnect")(function* (ctx: { params: { name: string } }) {
yield* mcp.disconnect(ctx.params.name)
yield* mcp
.disconnect(ctx.params.name)
.pipe(
Effect.catchTag("MCP.NotFoundError", (error) =>
Effect.fail(new McpServerNotFoundError({ name: error.name, message: `MCP server not found: ${error.name}` })),
),
)
return true
})