import { FetchHttpClient, HttpClient, HttpClientRequest, } from "@effect/platform"; import { applyCookiePlacementsToHeaders, applyHttpQueryPlacementsToUrl, } from "@executor/codemode-core"; import { buildOpenApiToolPresentation, compileOpenApiToolDefinitions, type OpenApiRefHintTable, type OpenApiToolProviderData, extractOpenApiManifest, } from "@executor/codemode-openapi"; import type { Source } from "#schema"; import { StringMapSchema } from "#schema"; import * as Effect from "effect/Effect"; import * as Schema from "effect/Schema"; import { createCatalogImportMetadata, createOpenApiCatalogFragment, type OpenApiCatalogOperationInput, } from "../source-catalog-snapshot"; import { createSourceCatalogSyncResult } from "../source-catalog-support"; import type { SourceAdapter } from "./types"; import { ConnectHttpAuthSchema, ConnectHttpImportAuthSchema, decodeBindingConfig, decodeSourceBindingPayload, emptySourceBindingState, encodeBindingConfig, isSourceCredentialRequiredError, OptionalNullableStringSchema, SourceCredentialRequiredError, SourceConnectCommonFieldsSchema, } from "./shared"; const OpenApiConnectPayloadSchema = Schema.extend( SourceConnectCommonFieldsSchema, Schema.extend( ConnectHttpImportAuthSchema, Schema.Struct({ kind: Schema.Literal("openapi"), specUrl: Schema.Trim.pipe(Schema.nonEmptyString()), auth: Schema.optional(ConnectHttpAuthSchema), }), ), ); const OpenApiExecutorAddInputSchema = Schema.extend( ConnectHttpImportAuthSchema, Schema.Struct({ kind: Schema.Literal("openapi"), endpoint: Schema.String, specUrl: Schema.String, name: OptionalNullableStringSchema, namespace: OptionalNullableStringSchema, auth: Schema.optional(ConnectHttpAuthSchema), }), ); const OpenApiBindingConfigSchema = Schema.Struct({ specUrl: Schema.Trim.pipe(Schema.nonEmptyString()), defaultHeaders: Schema.optional(Schema.NullOr(StringMapSchema)), }); const OpenApiSourceBindingPayloadSchema = Schema.Struct({ specUrl: Schema.optional(Schema.String), defaultHeaders: Schema.optional(Schema.NullOr(StringMapSchema)), }); type OpenApiBindingConfig = { specUrl: string; defaultHeaders: typeof StringMapSchema.Type | null; }; const OPENAPI_BINDING_CONFIG_VERSION = 1; const bindingHasAnyField = ( value: unknown, fields: readonly string[], ): boolean => value !== null && typeof value === "object" && !Array.isArray(value) && fields.some((field) => Object.prototype.hasOwnProperty.call(value, field)); const openApiBindingConfigFromSource = ( source: Pick, ): Effect.Effect => Effect.gen(function* () { if (bindingHasAnyField(source.binding, ["transport", "queryParams", "headers"])) { return yield* Effect.fail( new Error("OpenAPI sources cannot define MCP transport settings"), ); } const bindingConfig = yield* decodeSourceBindingPayload({ sourceId: source.id, label: "OpenAPI", version: source.bindingVersion, expectedVersion: OPENAPI_BINDING_CONFIG_VERSION, schema: OpenApiSourceBindingPayloadSchema, value: source.binding, allowedKeys: ["specUrl", "defaultHeaders"], }); const specUrl = typeof bindingConfig.specUrl === "string" ? bindingConfig.specUrl.trim() : ""; if (specUrl.length === 0) { return yield* Effect.fail(new Error("OpenAPI sources require specUrl")); } return { specUrl, defaultHeaders: bindingConfig.defaultHeaders ?? null, } satisfies OpenApiBindingConfig; }); const fetchOpenApiDocumentWithHeaders = (input: { url: string; headers?: Readonly>; queryParams?: Readonly>; cookies?: Readonly>; }): Effect.Effect => Effect.gen(function* () { const client = yield* HttpClient.HttpClient; const request = HttpClientRequest.get( applyHttpQueryPlacementsToUrl({ url: input.url, queryParams: input.queryParams, }).toString(), ).pipe( HttpClientRequest.setHeaders( applyCookiePlacementsToHeaders({ headers: input.headers ?? {}, cookies: input.cookies, }), ), ); const response = yield* client.execute(request).pipe( Effect.mapError((cause) => cause instanceof Error ? cause : new Error(String(cause)), ), ); if (response.status === 401 || response.status === 403) { return yield* Effect.fail( new SourceCredentialRequiredError( "import", `OpenAPI spec fetch requires credentials (status ${response.status})`, ), ); } if (response.status < 200 || response.status >= 300) { return yield* Effect.fail( new Error(`OpenAPI spec fetch failed with status ${response.status}`), ); } return yield* response.text.pipe( Effect.mapError((cause) => cause instanceof Error ? cause : new Error(String(cause)), ), ); }).pipe(Effect.provide(FetchHttpClient.layer)); const openApiCatalogOperationFromDefinition = ( input: { definition: ReturnType[number]; refHintTable?: Readonly; }, ): OpenApiCatalogOperationInput => { const presentation = buildOpenApiToolPresentation({ definition: input.definition, refHintTable: input.refHintTable, }); const method = input.definition.method.toUpperCase(); return { toolId: input.definition.toolId, title: input.definition.name, description: input.definition.description, effect: method === "GET" || method === "HEAD" ? "read" : method === "DELETE" ? "delete" : "write", inputSchema: presentation.inputSchema, outputSchema: presentation.outputSchema, providerData: presentation.providerData as OpenApiToolProviderData, }; }; export const openApiSourceAdapter: SourceAdapter = { key: "openapi", displayName: "OpenAPI", family: "http_api", bindingConfigVersion: OPENAPI_BINDING_CONFIG_VERSION, providerKey: "generic_http", defaultImportAuthPolicy: "reuse_runtime", connectPayloadSchema: OpenApiConnectPayloadSchema, executorAddInputSchema: OpenApiExecutorAddInputSchema, executorAddHelpText: [ "endpoint is the base API URL. specUrl is the OpenAPI document URL.", ], executorAddInputSignatureWidth: 420, serializeBindingConfig: (source) => encodeBindingConfig({ adapterKey: "openapi", version: OPENAPI_BINDING_CONFIG_VERSION, payloadSchema: OpenApiBindingConfigSchema, payload: Effect.runSync(openApiBindingConfigFromSource(source)), }), deserializeBindingConfig: ({ id, bindingConfigJson }) => Effect.map( decodeBindingConfig({ sourceId: id, label: "OpenAPI", adapterKey: "openapi", version: OPENAPI_BINDING_CONFIG_VERSION, payloadSchema: OpenApiBindingConfigSchema, value: bindingConfigJson, }), ({ version, payload }) => ({ version, payload: { specUrl: payload.specUrl, defaultHeaders: payload.defaultHeaders ?? null, }, }), ), bindingStateFromSource: (source) => Effect.map(openApiBindingConfigFromSource(source), (bindingConfig) => ({ ...emptySourceBindingState, specUrl: bindingConfig.specUrl, defaultHeaders: bindingConfig.defaultHeaders, })), sourceConfigFromSource: (source) => Effect.runSync( Effect.map(openApiBindingConfigFromSource(source), (bindingConfig) => ({ kind: "openapi", endpoint: source.endpoint, specUrl: bindingConfig.specUrl, defaultHeaders: bindingConfig.defaultHeaders, })), ), validateSource: (source) => Effect.gen(function* () { const bindingConfig = yield* openApiBindingConfigFromSource(source); return { ...source, bindingVersion: OPENAPI_BINDING_CONFIG_VERSION, binding: { specUrl: bindingConfig.specUrl, defaultHeaders: bindingConfig.defaultHeaders, }, }; }), shouldAutoProbe: (source) => source.enabled && (source.status === "draft" || source.status === "probing"), syncCatalog: ({ source, resolveAuthMaterialForSlot }) => Effect.gen(function* () { const bindingConfig = yield* openApiBindingConfigFromSource(source); const auth = yield* resolveAuthMaterialForSlot("import"); const openApiDocument = yield* fetchOpenApiDocumentWithHeaders({ url: bindingConfig.specUrl, headers: { ...(bindingConfig.defaultHeaders ?? {}), ...auth.headers, }, queryParams: auth.queryParams, cookies: auth.cookies, }).pipe( Effect.mapError( (cause) => isSourceCredentialRequiredError(cause) ? cause : new Error( `Failed fetching OpenAPI spec for ${source.id}: ${cause.message}`, ), ), ); const manifest = yield* extractOpenApiManifest( source.name, openApiDocument, ).pipe( Effect.mapError((cause) => cause instanceof Error ? cause : new Error(String(cause)), ), ); const definitions = compileOpenApiToolDefinitions(manifest); const now = Date.now(); return createSourceCatalogSyncResult({ fragment: createOpenApiCatalogFragment({ source, documents: [{ documentKind: "openapi", documentKey: bindingConfig.specUrl, contentText: openApiDocument, fetchedAt: now, }], operations: definitions.map((definition) => openApiCatalogOperationFromDefinition({ definition, refHintTable: manifest.refHintTable, })), }), importMetadata: createCatalogImportMetadata({ source, adapterKey: "openapi", }), sourceHash: manifest.sourceHash, }); }), };