Introduce POST /sources/connect that handles MCP, OpenAPI, and GraphQL connection in a single endpoint (replacing the old MCP-only connect). Add POST /sources/discover for URL-based source type detection. Update sources-operations with auto-probe logic for draft sources.
Unified · Split
| @@ -1,14 +1,17 @@ | ||
| 1 | 1 | import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema } from "@effect/platform"; |
| 2 | 2 | import { |
| 3 | 3 | ExecutionInteractionIdSchema, |
| 4 | + SecretRefSchema, | |
| 4 | 5 | SourceAuthSchema, |
| 5 | 6 | SourceAuthSessionIdSchema, |
| 7 | + SourceDiscoveryResultSchema, | |
| 6 | 8 | SourceIdSchema, |
| 7 | 9 | SourceInspectionDiscoverPayloadSchema, |
| 8 | 10 | SourceInspectionDiscoverResultSchema, |
| 9 | 11 | SourceInspectionSchema, |
| 10 | 12 | SourceInspectionToolDetailSchema, |
| 11 | 13 | SourceKindSchema, |
| 14 | + SourceProbeAuthSchema, | |
| 12 | 15 | SourceSchema, |
| 13 | 16 | SourceStatusSchema, |
| 14 | 17 | SourceTransportSchema, |
| @@ -75,34 +78,6 @@ export const UpdateSourcePayloadSchema = Schema.Struct({ · 60 unmodified lines | ||
| 75 | 78 | |
| 76 | 79 | export type UpdateSourcePayload = typeof UpdateSourcePayloadSchema.Type; |
| 77 | 80 | |
| 78 | −export const ConnectMcpSourcePayloadSchema = Schema.Struct({ | |
| 79 | − sourceId: Schema.optional(SourceIdSchema), | |
| 80 | − name: Schema.optional(Schema.NullOr(Schema.String)), | |
| 81 | − endpoint: TrimmedNonEmptyStringSchema, | |
| 82 | − namespace: Schema.optional(Schema.NullOr(Schema.String)), | |
| 83 | − enabled: Schema.optional(Schema.Boolean), | |
| 84 | − transport: Schema.optional(SourceTransportSchema), | |
| 85 | − queryParams: Schema.optional(Schema.NullOr(StringMapSchema)), | |
| 86 | − headers: Schema.optional(Schema.NullOr(StringMapSchema)), | |
| 87 | −}); | |
| 88 | − | |
| 89 | −export type ConnectMcpSourcePayload = typeof ConnectMcpSourcePayloadSchema.Type; | |
| 90 | − | |
| 91 | −export const ConnectMcpSourceResultSchema = Schema.Union( | |
| 92 | − Schema.Struct({ | |
| 93 | − kind: Schema.Literal("connected"), | |
| 94 | − source: SourceSchema, | |
| 95 | − }), | |
| 96 | − Schema.Struct({ | |
| 97 | − kind: Schema.Literal("oauth_required"), | |
| 98 | − source: SourceSchema, | |
| 99 | − sessionId: SourceAuthSessionIdSchema, | |
| 100 | − authorizationUrl: Schema.String, | |
| 101 | − }), | |
| 102 | −); | |
| 103 | − | |
| 104 | −export type ConnectMcpSourceResult = typeof ConnectMcpSourceResultSchema.Type; | |
| 105 | − | |
| 106 | 81 | const workspaceIdParam = HttpApiSchema.param("workspaceId", WorkspaceIdSchema); |
| 107 | 82 | const sourceIdParam = HttpApiSchema.param("sourceId", SourceIdSchema); |
| 108 | 83 | const toolPathParam = HttpApiSchema.param("toolPath", Schema.String); |
| @@ -123,11 +98,113 @@ const CredentialOauthCompleteUrlParamsSchema = Schema.Struct({ · 14 unmodified lines | ||
| 123 | 98 | error_description: Schema.optional(Schema.String), |
| 124 | 99 | }); |
| 125 | 100 | |
| 101 | +const DiscoverSourcePayloadSchema = Schema.Struct({ | |
| 102 | + url: TrimmedNonEmptyStringSchema, | |
| 103 | + probeAuth: Schema.optional(SourceProbeAuthSchema), | |
| 104 | +}); | |
| 105 | + | |
| 106 | +export type DiscoverSourcePayload = typeof DiscoverSourcePayloadSchema.Type; | |
| 107 | + | |
| 108 | +const ConnectBearerAuthSchema = Schema.Struct({ | |
| 109 | + kind: Schema.Literal("bearer"), | |
| 110 | + headerName: Schema.optional(Schema.NullOr(Schema.String)), | |
| 111 | + prefix: Schema.optional(Schema.NullOr(Schema.String)), | |
| 112 | + token: Schema.optional(Schema.NullOr(Schema.String)), | |
| 113 | + tokenRef: Schema.optional(Schema.NullOr(SecretRefSchema)), | |
| 114 | +}); | |
| 115 | + | |
| 116 | +const ConnectOAuth2AuthSchema = Schema.Struct({ | |
| 117 | + kind: Schema.Literal("oauth2"), | |
| 118 | + headerName: Schema.optional(Schema.NullOr(Schema.String)), | |
| 119 | + prefix: Schema.optional(Schema.NullOr(Schema.String)), | |
| 120 | + accessToken: Schema.optional(Schema.NullOr(Schema.String)), | |
| 121 | + accessTokenRef: Schema.optional(Schema.NullOr(SecretRefSchema)), | |
| 122 | + refreshToken: Schema.optional(Schema.NullOr(Schema.String)), | |
| 123 | + refreshTokenRef: Schema.optional(Schema.NullOr(SecretRefSchema)), | |
| 124 | +}); | |
| 125 | + | |
| 126 | +const ConnectHttpAuthSchema = Schema.Union( | |
| 127 | + Schema.Struct({ kind: Schema.Literal("none") }), | |
| 128 | + ConnectBearerAuthSchema, | |
| 129 | + ConnectOAuth2AuthSchema, | |
| 130 | +); | |
| 131 | + | |
| 132 | +const ConnectSourcePayloadSchema = Schema.Union( | |
| 133 | + Schema.Struct({ | |
| 134 | + kind: Schema.Literal("openapi"), | |
| 135 | + endpoint: TrimmedNonEmptyStringSchema, | |
| 136 | + specUrl: TrimmedNonEmptyStringSchema, | |
| 137 | + name: Schema.optional(Schema.NullOr(Schema.String)), | |
| 138 | + namespace: Schema.optional(Schema.NullOr(Schema.String)), | |
| 139 | + auth: Schema.optional(ConnectHttpAuthSchema), | |
| 140 | + }), | |
| 141 | + Schema.Struct({ | |
| 142 | + kind: Schema.Literal("graphql"), | |
| 143 | + endpoint: TrimmedNonEmptyStringSchema, | |
| 144 | + name: Schema.optional(Schema.NullOr(Schema.String)), | |
| 145 | + namespace: Schema.optional(Schema.NullOr(Schema.String)), | |
| 146 | + auth: Schema.optional(ConnectHttpAuthSchema), | |
| 147 | + }), | |
| 148 | + Schema.Struct({ | |
| 149 | + kind: Schema.Literal("mcp"), | |
| 150 | + endpoint: TrimmedNonEmptyStringSchema, | |
| 151 | + name: Schema.optional(Schema.NullOr(Schema.String)), | |
| 152 | + namespace: Schema.optional(Schema.NullOr(Schema.String)), | |
| 153 | + transport: Schema.optional(Schema.NullOr(SourceTransportSchema)), | |
| 154 | + queryParams: Schema.optional(Schema.NullOr(StringMapSchema)), | |
| 155 | + headers: Schema.optional(Schema.NullOr(StringMapSchema)), | |
| 156 | + }), | |
| 157 | +); | |
| 158 | + | |
| 159 | +export type ConnectSourcePayload = typeof ConnectSourcePayloadSchema.Type; | |
| 160 | + | |
| 161 | +const ConnectSourceResultSchema = Schema.Union( | |
| 162 | + Schema.Struct({ | |
| 163 | + kind: Schema.Literal("connected"), | |
| 164 | + source: SourceSchema, | |
| 165 | + }), | |
| 166 | + Schema.Struct({ | |
| 167 | + kind: Schema.Literal("credential_required"), | |
| 168 | + source: SourceSchema, | |
| 169 | + }), | |
| 170 | + Schema.Struct({ | |
| 171 | + kind: Schema.Literal("oauth_required"), | |
| 172 | + source: SourceSchema, | |
| 173 | + sessionId: SourceAuthSessionIdSchema, | |
| 174 | + authorizationUrl: Schema.String, | |
| 175 | + }), | |
| 176 | +); | |
| 177 | + | |
| 178 | +export type ConnectSourceResult = typeof ConnectSourceResultSchema.Type; | |
| 179 | + | |
| 180 | +export { | |
| 181 | + ConnectSourcePayloadSchema, | |
| 182 | + ConnectSourceResultSchema, | |
| 183 | + DiscoverSourcePayloadSchema, | |
| 184 | +}; | |
| 185 | + | |
| 126 | 186 | const HtmlSchema = HttpApiSchema.Text({ |
| 127 | 187 | contentType: "text/html", |
| 128 | 188 | }); |
| 129 | 189 | |
| 130 | 190 | export class SourcesApi extends HttpApiGroup.make("sources") |
| 191 | + .add( | |
| 192 | + HttpApiEndpoint.post("discover")`/sources/discover` | |
| 193 | + .setPayload(DiscoverSourcePayloadSchema) | |
| 194 | + .addSuccess(SourceDiscoveryResultSchema) | |
| 195 | + .addError(ControlPlaneBadRequestError) | |
| 196 | + .addError(ControlPlaneUnauthorizedError) | |
| 197 | + .addError(ControlPlaneForbiddenError), | |
| 198 | + ) | |
| 199 | + .add( | |
| 200 | + HttpApiEndpoint.post("connect")`/workspaces/${workspaceIdParam}/sources/connect` | |
| 201 | + .setPayload(ConnectSourcePayloadSchema) | |
| 202 | + .addSuccess(ConnectSourceResultSchema) | |
| 203 | + .addError(ControlPlaneBadRequestError) | |
| 204 | + .addError(ControlPlaneUnauthorizedError) | |
| 205 | + .addError(ControlPlaneForbiddenError) | |
| 206 | + .addError(ControlPlaneStorageError), | |
| 207 | + ) | |
| 131 | 208 | .add( |
| 132 | 209 | HttpApiEndpoint.get("list")`/workspaces/${workspaceIdParam}/sources` |
| 133 | 210 | .addSuccess(Schema.Array(SourceSchema)) |
| @@ -164,16 +241,6 @@ export class SourcesApi extends HttpApiGroup.make("sources") · 30 unmodified lines | ||
| 164 | 241 | .addError(ControlPlaneNotFoundError) |
| 165 | 242 | .addError(ControlPlaneStorageError), |
| 166 | 243 | ) |
| 167 | − .add( | |
| 168 | − HttpApiEndpoint.post("connectMcp")`/workspaces/${workspaceIdParam}/sources/mcp/oauth/start` | |
| 169 | − .setPayload(ConnectMcpSourcePayloadSchema) | |
| 170 | − .addSuccess(ConnectMcpSourceResultSchema) | |
| 171 | − .addError(ControlPlaneBadRequestError) | |
| 172 | − .addError(ControlPlaneUnauthorizedError) | |
| 173 | − .addError(ControlPlaneForbiddenError) | |
| 174 | − .addError(ControlPlaneNotFoundError) | |
| 175 | − .addError(ControlPlaneStorageError), | |
| 176 | − ) | |
| 177 | 244 | .add( |
| 178 | 245 | HttpApiEndpoint.del("remove")`/workspaces/${workspaceIdParam}/sources/${sourceIdParam}` |
| 179 | 246 | .addSuccess(Schema.Struct({ removed: Schema.Boolean })) |
| @@ -1,5 +1,6 @@ | ||
| 1 | 1 | import { |
| 2 | 2 | HttpApiBuilder, |
| 3 | + HttpServerRequest, | |
| 3 | 4 | HttpServerResponse, |
| 4 | 5 | } from "@effect/platform"; |
| 5 | 6 | import type { |
| @@ -11,7 +12,6 @@ import * as Effect from "effect/Effect"; · 5 unmodified lines | ||
| 11 | 12 | |
| 12 | 13 | import { requirePermission, withPolicy } from "#domain"; |
| 13 | 14 | import { |
| 14 | − connectMcpSource, | |
| 15 | 15 | createSource, |
| 16 | 16 | getSource, |
| 17 | 17 | listSources, |
| @@ -28,6 +28,8 @@ import { · 10 unmodified lines | ||
| 28 | 28 | getSourceCredentialInteraction, |
| 29 | 29 | submitSourceCredentialInteraction, |
| 30 | 30 | } from "../../runtime/local-operations"; |
| 31 | +import { discoverSource } from "../../runtime/source-discovery"; | |
| 32 | +import { RuntimeSourceAuthServiceTag } from "../../runtime/source-auth-service"; | |
| 31 | 33 | |
| 32 | 34 | import { |
| 33 | 35 | ControlPlaneBadRequestError, |
| @@ -35,7 +37,7 @@ import { · 1 unmodified line | ||
| 35 | 37 | ControlPlaneStorageError, |
| 36 | 38 | } from "../errors"; |
| 37 | 39 | import { ControlPlaneApi } from "../api"; |
| 38 | −import { withWorkspaceRequestActor } from "../http-auth"; | |
| 40 | +import { withRequestActor, withWorkspaceRequestActor } from "../http-auth"; | |
| 39 | 41 | |
| 40 | 42 | const requireReadSources = (workspaceId: WorkspaceId) => |
| 41 | 43 | requirePermission({ |
| @@ -49,6 +51,50 @@ const requireWriteSources = (workspaceId: WorkspaceId) => · 7 unmodified lines | ||
| 49 | 51 | workspaceId, |
| 50 | 52 | }); |
| 51 | 53 | |
| 54 | +const readHeader = (headers: unknown, name: string): string | null => { | |
| 55 | + if (headers == null || typeof headers !== "object") { | |
| 56 | + return null; | |
| 57 | + } | |
| 58 | + | |
| 59 | + const record = headers as Record<string, unknown>; | |
| 60 | + const exact = record[name]; | |
| 61 | + if (typeof exact === "string" && exact.length > 0) { | |
| 62 | + return exact; | |
| 63 | + } | |
| 64 | + | |
| 65 | + const lower = record[name.toLowerCase()]; | |
| 66 | + if (typeof lower === "string" && lower.length > 0) { | |
| 67 | + return lower; | |
| 68 | + } | |
| 69 | + | |
| 70 | + return null; | |
| 71 | +}; | |
| 72 | + | |
| 73 | +const resolveRequestOrigin = (request: { url: string; headers: unknown }): string | null => { | |
| 74 | + try { | |
| 75 | + return new URL(request.url).origin; | |
| 76 | + } catch { | |
| 77 | + const forwardedHost = readHeader(request.headers, "x-forwarded-host"); | |
| 78 | + const host = forwardedHost ?? readHeader(request.headers, "host"); | |
| 79 | + if (!host) { | |
| 80 | + return null; | |
| 81 | + } | |
| 82 | + | |
| 83 | + const forwardedProto = readHeader(request.headers, "x-forwarded-proto"); | |
| 84 | + const protocol = forwardedProto && forwardedProto.length > 0 ? forwardedProto : "http"; | |
| 85 | + return `${protocol}://${host}`; | |
| 86 | + } | |
| 87 | +}; | |
| 88 | + | |
| 89 | +const toBadRequestError = (operation: string, cause: unknown) => { | |
| 90 | + const message = cause instanceof Error ? cause.message : String(cause); | |
| 91 | + return new ControlPlaneBadRequestError({ | |
| 92 | + operation, | |
| 93 | + message, | |
| 94 | + details: message, | |
| 95 | + }); | |
| 96 | +}; | |
| 97 | + | |
| 52 | 98 | const escapeHtml = (value: string): string => |
| 53 | 99 | value |
| 54 | 100 | .replaceAll("&", "&") |
| @@ -616,6 +662,77 @@ export const ControlPlaneSourcesLive = HttpApiBuilder.group( · 561 unmodified lines | ||
| 616 | 662 | "sources", |
| 617 | 663 | (handlers) => |
| 618 | 664 | handlers |
| 665 | + .handle("discover", ({ payload }) => | |
| 666 | + withRequestActor("sources.discover", () => | |
| 667 | + discoverSource({ | |
| 668 | + url: payload.url, | |
| 669 | + probeAuth: payload.probeAuth, | |
| 670 | + }).pipe( | |
| 671 | + Effect.catchAll((cause) => | |
| 672 | + Effect.fail(toBadRequestError("sources.discover", cause)), | |
| 673 | + ), | |
| 674 | + ), | |
| 675 | + ), | |
| 676 | + ) | |
| 677 | + .handle("connect", ({ path, payload }) => | |
| 678 | + withWorkspaceRequestActor("sources.connect", path.workspaceId, () => | |
| 679 | + withPolicy(requireWriteSources(path.workspaceId))( | |
| 680 | + Effect.gen(function* () { | |
| 681 | + const request = yield* HttpServerRequest.HttpServerRequest; | |
| 682 | + const sourceAuthService = yield* RuntimeSourceAuthServiceTag; | |
| 683 | + const baseUrl = resolveRequestOrigin(request); | |
| 684 | + | |
| 685 | + if (payload.kind === "mcp") { | |
| 686 | + return yield* sourceAuthService.connectMcpSource({ | |
| 687 | + workspaceId: path.workspaceId, | |
| 688 | + endpoint: payload.endpoint, | |
| 689 | + name: payload.name, | |
| 690 | + namespace: payload.namespace, | |
| 691 | + transport: payload.transport, | |
| 692 | + queryParams: payload.queryParams, | |
| 693 | + headers: payload.headers, | |
| 694 | + baseUrl, | |
| 695 | + }); | |
| 696 | + } | |
| 697 | + | |
| 698 | + if (payload.kind === "openapi") { | |
| 699 | + return yield* sourceAuthService.addExecutorSource( | |
| 700 | + { | |
| 701 | + workspaceId: path.workspaceId, | |
| 702 | + executionId: null, | |
| 703 | + interactionId: null, | |
| 704 | + kind: "openapi", | |
| 705 | + endpoint: payload.endpoint, | |
| 706 | + specUrl: payload.specUrl, | |
| 707 | + name: payload.name, | |
| 708 | + namespace: payload.namespace, | |
| 709 | + auth: payload.auth, | |
| 710 | + }, | |
| 711 | + { baseUrl }, | |
| 712 | + ); | |
| 713 | + } | |
| 714 | + | |
| 715 | + return yield* sourceAuthService.addExecutorSource( | |
| 716 | + { | |
| 717 | + workspaceId: path.workspaceId, | |
| 718 | + executionId: null, | |
| 719 | + interactionId: null, | |
| 720 | + kind: "graphql", | |
| 721 | + endpoint: payload.endpoint, | |
| 722 | + name: payload.name, | |
| 723 | + namespace: payload.namespace, | |
| 724 | + auth: payload.auth, | |
| 725 | + }, | |
| 726 | + { baseUrl }, | |
| 727 | + ); | |
| 728 | + }).pipe( | |
| 729 | + Effect.catchAll((cause) => | |
| 730 | + Effect.fail(toBadRequestError("sources.connect", cause)), | |
| 731 | + ), | |
| 732 | + ), | |
| 733 | + ), | |
| 734 | + ), | |
| 735 | + ) | |
| 619 | 736 | .handle("list", ({ path }) => |
| 620 | 737 | withWorkspaceRequestActor("sources.list", path.workspaceId, () => |
| 621 | 738 | withPolicy(requireReadSources(path.workspaceId))( |
| @@ -683,16 +800,6 @@ export const ControlPlaneSourcesLive = HttpApiBuilder.group( · 61 unmodified lines | ||
| 683 | 800 | ), |
| 684 | 801 | ), |
| 685 | 802 | ) |
| 686 | − .handle("connectMcp", ({ path, payload }) => | |
| 687 | − withWorkspaceRequestActor("sources.connectMcp", path.workspaceId, () => | |
| 688 | − withPolicy(requireWriteSources(path.workspaceId))( | |
| 689 | − connectMcpSource({ | |
| 690 | − workspaceId: path.workspaceId, | |
| 691 | − payload, | |
| 692 | − }), | |
| 693 | − ), | |
| 694 | − ), | |
| 695 | − ) | |
| 696 | 803 | .handle("remove", ({ path }) => |
| 697 | 804 | withWorkspaceRequestActor("sources.remove", path.workspaceId, () => |
| 698 | 805 | withPolicy(requireWriteSources(path.workspaceId))( |
| @@ -1,5 +1,4 @@ | ||
| 1 | 1 | import type { |
| 2 | − ConnectMcpSourcePayload, | |
| 3 | 2 | CreateSourcePayload, |
| 4 | 3 | UpdateSourcePayload, |
| 5 | 4 | } from "../api/sources/api"; |
| @@ -24,7 +23,6 @@ import { · 18 unmodified lines | ||
| 24 | 23 | operationErrors, |
| 25 | 24 | } from "./operation-errors"; |
| 26 | 25 | import { createDefaultSecretMaterialResolver } from "./secret-material-providers"; |
| 27 | −import { RuntimeSourceAuthServiceTag } from "./source-auth-service"; | |
| 28 | 26 | import { ControlPlaneStore, type ControlPlaneStoreShape } from "./store"; |
| 29 | 27 | import { syncSourceToolArtifacts } from "./tool-artifacts"; |
| 30 | 28 | import { |
| @@ -39,10 +37,17 @@ const sourceOps = { · 8 unmodified lines | ||
| 39 | 37 | create: operationErrors("sources.create"), |
| 40 | 38 | get: operationErrors("sources.get"), |
| 41 | 39 | update: operationErrors("sources.update"), |
| 42 | − connectMcp: operationErrors("sources.connectMcp"), | |
| 43 | 40 | remove: operationErrors("sources.remove"), |
| 44 | 41 | } as const; |
| 45 | 42 | |
| 43 | +const shouldAutoProbeSource = (source: Source): boolean => | |
| 44 | + source.enabled | |
| 45 | + && ( | |
| 46 | + (source.kind === "openapi" && !!source.specUrl) | |
| 47 | + || source.kind === "graphql" | |
| 48 | + ) | |
| 49 | + && (source.status === "draft" || source.status === "probing"); | |
| 50 | + | |
| 46 | 51 | const syncArtifactsForSource = (input: { |
| 47 | 52 | store: ControlPlaneStoreShape; |
| 48 | 53 | source: Source; |
| @@ -55,19 +60,50 @@ const syncArtifactsForSource = (input: { · 6 unmodified lines | ||
| 55 | 60 | rows: input.store, |
| 56 | 61 | }); |
| 57 | 62 | |
| 63 | + // For HTTP-backed source kinds that can validate themselves from a remote | |
| 64 | + // document, automatically attempt to probe and connect. This mirrors the | |
| 65 | + // addExecutorSource flow by overriding status to "connected" so the sync | |
| 66 | + // guard passes. | |
| 67 | + const autoProbe = shouldAutoProbeSource(input.source); | |
| 68 | + const sourceForSync = autoProbe | |
| 69 | + ? { ...input.source, status: "connected" as const } | |
| 70 | + : input.source; | |
| 71 | + | |
| 58 | 72 | const synced = yield* Effect.either( |
| 59 | 73 | syncSourceToolArtifacts({ |
| 60 | 74 | rows: input.store, |
| 61 | − source: input.source, | |
| 75 | + source: sourceForSync, | |
| 62 | 76 | resolveSecretMaterial, |
| 63 | 77 | }), |
| 64 | 78 | ); |
| 65 | 79 | |
| 66 | 80 | return yield* Either.match(synced, { |
| 67 | − onRight: () => Effect.succeed(input.source), | |
| 81 | + onRight: () => | |
| 82 | + Effect.gen(function* () { | |
| 83 | + if (autoProbe) { | |
| 84 | + const connectedSource = yield* updateSourceFromPayload({ | |
| 85 | + source: input.source, | |
| 86 | + payload: { status: "connected", lastError: null }, | |
| 87 | + now: Date.now(), | |
| 88 | + }).pipe( | |
| 89 | + Effect.mapError((cause) => | |
| 90 | + input.operation.badRequest( | |
| 91 | + "Failed updating source status", | |
| 92 | + cause instanceof Error ? cause.message : String(cause), | |
| 93 | + ), | |
| 94 | + ), | |
| 95 | + ); | |
| 96 | + yield* mapPersistenceError( | |
| 97 | + input.operation.child("source_connected"), | |
| 98 | + persistSource(input.store, connectedSource), | |
| 99 | + ); | |
| 100 | + return connectedSource; | |
| 101 | + } | |
| 102 | + return input.source; | |
| 103 | + }), | |
| 68 | 104 | onLeft: (error) => |
| 69 | 105 | Effect.gen(function* () { |
| 70 | − if (input.source.enabled && input.source.status === "connected") { | |
| 106 | + if (autoProbe || (input.source.enabled && input.source.status === "connected")) { | |
| 71 | 107 | const erroredSource = yield* updateSourceFromPayload({ |
| 72 | 108 | source: input.source, |
| 73 | 109 | payload: { |
| @@ -213,40 +249,6 @@ export const updateSource = (input: { · 139 unmodified lines | ||
| 213 | 249 | }); |
| 214 | 250 | })); |
| 215 | 251 | |
| 216 | −export const connectMcpSource = (input: { | |
| 217 | − workspaceId: WorkspaceId; | |
| 218 | − payload: ConnectMcpSourcePayload; | |
| 219 | −}) => | |
| 220 | − Effect.gen(function* () { | |
| 221 | − const sourceAuthService = yield* RuntimeSourceAuthServiceTag; | |
| 222 | − | |
| 223 | − return yield* sourceAuthService.connectMcpSource({ | |
| 224 | − workspaceId: input.workspaceId, | |
| 225 | − sourceId: input.payload.sourceId, | |
| 226 | − endpoint: input.payload.endpoint, | |
| 227 | − name: input.payload.name, | |
| 228 | − namespace: input.payload.namespace, | |
| 229 | − enabled: input.payload.enabled, | |
| 230 | − transport: input.payload.transport, | |
| 231 | − queryParams: input.payload.queryParams, | |
| 232 | − headers: input.payload.headers, | |
| 233 | − }).pipe( | |
| 234 | − Effect.mapError((cause) => { | |
| 235 | − if (cause instanceof Error && cause.message.startsWith("Source not found:")) { | |
| 236 | − return sourceOps.connectMcp.notFound( | |
| 237 | − "Source not found", | |
| 238 | − `workspaceId=${input.workspaceId} sourceId=${input.payload.sourceId}`, | |
| 239 | − ); | |
| 240 | − } | |
| 241 | − | |
| 242 | − return sourceOps.connectMcp.unknownStorage( | |
| 243 | − cause, | |
| 244 | − "Failed preparing MCP source OAuth flow", | |
| 245 | − ); | |
| 246 | − }), | |
| 247 | − ); | |
| 248 | − }); | |
| 249 | − | |
| 250 | 252 | export const removeSource = (input: { |
| 251 | 253 | workspaceId: WorkspaceId; |
| 252 | 254 | sourceId: SourceId; |