Unified · Split
| @@ -51,6 +51,7 @@ import { · 50 unmodified lines | |||
| 51 | type McpTransportFields, | 51 | type McpTransportFields, |
| 52 | type McpTransportValue, | 52 | type McpTransportValue, |
| 53 | } from "./mcp-transport-state"; | 53 | } from "./mcp-transport-state"; |
| 54 | import { parseJsonStringArray, parseJsonStringMap } from "./json-form"; | ||
| 54 | import { getDomain } from "tldts"; | 55 | import { getDomain } from "tldts"; |
| 55 | 56 | ||
| 56 | // --------------------------------------------------------------------------- | 57 | // --------------------------------------------------------------------------- |
| @@ -214,68 +215,6 @@ const googleDiscoveryDefaultsFromUrl = ( · 157 unmodified lines | |||
| 214 | } | 215 | } |
| 215 | }; | 216 | }; |
| 216 | 217 | ||
| 217 | const parseJsonStringMap = ( | ||
| 218 | label: string, | ||
| 219 | text: string, | ||
| 220 | ): Record<string, string> | null => { | ||
| 221 | const trimmed = text.trim(); | ||
| 222 | if (trimmed.length === 0) { | ||
| 223 | return null; | ||
| 224 | } | ||
| 225 | |||
| 226 | let parsed: unknown; | ||
| 227 | try { | ||
| 228 | parsed = JSON.parse(trimmed); | ||
| 229 | } catch { | ||
| 230 | throw new Error(`${label} must be valid JSON.`); | ||
| 231 | } | ||
| 232 | |||
| 233 | if (parsed === null || Array.isArray(parsed) || typeof parsed !== "object") { | ||
| 234 | throw new Error(`${label} must be a JSON object with string values.`); | ||
| 235 | } | ||
| 236 | |||
| 237 | const entries = Object.entries(parsed as Record<string, unknown>); | ||
| 238 | const normalized: Record<string, string> = {}; | ||
| 239 | for (const [key, value] of entries) { | ||
| 240 | if (typeof value !== "string") { | ||
| 241 | throw new Error(`${label} must only contain string values.`); | ||
| 242 | } | ||
| 243 | normalized[key] = value; | ||
| 244 | } | ||
| 245 | |||
| 246 | return Object.keys(normalized).length === 0 ? null : normalized; | ||
| 247 | }; | ||
| 248 | |||
| 249 | const parseJsonStringArray = ( | ||
| 250 | label: string, | ||
| 251 | text: string, | ||
| 252 | ): Array<string> | null => { | ||
| 253 | const trimmed = text.trim(); | ||
| 254 | if (trimmed.length === 0) { | ||
| 255 | return null; | ||
| 256 | } | ||
| 257 | |||
| 258 | let parsed: unknown; | ||
| 259 | try { | ||
| 260 | parsed = JSON.parse(trimmed); | ||
| 261 | } catch { | ||
| 262 | throw new Error(`${label} must be valid JSON.`); | ||
| 263 | } | ||
| 264 | |||
| 265 | if ( | ||
| 266 | !Array.isArray(parsed) || | ||
| 267 | !parsed.every((value) => typeof value === "string") | ||
| 268 | ) { | ||
| 269 | throw new Error(`${label} must be a JSON array of strings.`); | ||
| 270 | } | ||
| 271 | |||
| 272 | const normalized = parsed | ||
| 273 | .map((value) => value.trim()) | ||
| 274 | .filter((value) => value.length > 0); | ||
| 275 | |||
| 276 | return normalized.length > 0 ? normalized : null; | ||
| 277 | }; | ||
| 278 | |||
| 279 | const stringifyStringMap = ( | 218 | const stringifyStringMap = ( |
| 280 | value: Readonly<Record<string, string>> | null | undefined, | 219 | value: Readonly<Record<string, string>> | null | undefined, |
| 281 | ): string => | 220 | ): string => |
| @@ -0,0 +1,74 @@ | |||
| 1 | import * as Either from "effect/Either"; | ||
| 2 | import * as ParseResult from "effect/ParseResult"; | ||
| 3 | import * as Schema from "effect/Schema"; | ||
| 4 | |||
| 5 | const JsonStringMapSchema = Schema.parseJson( | ||
| 6 | Schema.Record({ | ||
| 7 | key: Schema.String, | ||
| 8 | value: Schema.String, | ||
| 9 | }), | ||
| 10 | ); | ||
| 11 | |||
| 12 | const JsonStringArraySchema = Schema.parseJson( | ||
| 13 | Schema.Array(Schema.String), | ||
| 14 | ); | ||
| 15 | |||
| 16 | const formatJsonFieldError = ( | ||
| 17 | label: string, | ||
| 18 | error: ParseResult.ParseError, | ||
| 19 | ): Error => | ||
| 20 | new Error( | ||
| 21 | `${label} is invalid: ${ParseResult.TreeFormatter.formatErrorSync(error)}`, | ||
| 22 | ); | ||
| 23 | |||
| 24 | const decodeJsonField = <A>(input: { | ||
| 25 | label: string; | ||
| 26 | text: string; | ||
| 27 | schema: Schema.Schema<A, string>; | ||
| 28 | }): A | null => { | ||
| 29 | const trimmed = input.text.trim(); | ||
| 30 | if (trimmed.length === 0) { | ||
| 31 | return null; | ||
| 32 | } | ||
| 33 | |||
| 34 | const decoded = Schema.decodeUnknownEither(input.schema)(trimmed); | ||
| 35 | if (Either.isLeft(decoded)) { | ||
| 36 | throw formatJsonFieldError(input.label, decoded.left); | ||
| 37 | } | ||
| 38 | |||
| 39 | return decoded.right; | ||
| 40 | }; | ||
| 41 | |||
| 42 | export const parseJsonStringMap = ( | ||
| 43 | label: string, | ||
| 44 | text: string, | ||
| 45 | ): Record<string, string> | null => { | ||
| 46 | const decoded = decodeJsonField({ | ||
| 47 | label, | ||
| 48 | text, | ||
| 49 | schema: JsonStringMapSchema, | ||
| 50 | }); | ||
| 51 | |||
| 52 | return decoded && Object.keys(decoded).length > 0 ? decoded : null; | ||
| 53 | }; | ||
| 54 | |||
| 55 | export const parseJsonStringArray = ( | ||
| 56 | label: string, | ||
| 57 | text: string, | ||
| 58 | ): Array<string> | null => { | ||
| 59 | const decoded = decodeJsonField({ | ||
| 60 | label, | ||
| 61 | text, | ||
| 62 | schema: JsonStringArraySchema, | ||
| 63 | }); | ||
| 64 | |||
| 65 | if (!decoded) { | ||
| 66 | return null; | ||
| 67 | } | ||
| 68 | |||
| 69 | const normalized = decoded | ||
| 70 | .map((value) => value.trim()) | ||
| 71 | .filter((value) => value.length > 0); | ||
| 72 | |||
| 73 | return normalized.length > 0 ? normalized : null; | ||
| 74 | }; | ||
| @@ -44,6 +44,7 @@ import { · 43 unmodified lines | |||
| 44 | type McpTransportFields, | 44 | type McpTransportFields, |
| 45 | type McpTransportValue, | 45 | type McpTransportValue, |
| 46 | } from "./mcp-transport-state"; | 46 | } from "./mcp-transport-state"; |
| 47 | import { parseJsonStringArray, parseJsonStringMap } from "./json-form"; | ||
| 47 | import { sourceTemplates, type SourceTemplate } from "./source-templates"; | 48 | import { sourceTemplates, type SourceTemplate } from "./source-templates"; |
| 48 | import { getDomain } from "tldts"; | 49 | import { getDomain } from "tldts"; |
| 49 | 50 | ||
| @@ -357,36 +358,6 @@ const readBindingTransport = (source: Source): McpTransportValue => { · 307 unmodified lines | |||
| 357 | : ""; | 358 | : ""; |
| 358 | }; | 359 | }; |
| 359 | 360 | ||
| 360 | const parseJsonStringArray = ( | ||
| 361 | label: string, | ||
| 362 | text: string, | ||
| 363 | ): Array<string> | null => { | ||
| 364 | const trimmed = text.trim(); | ||
| 365 | if (trimmed.length === 0) { | ||
| 366 | return null; | ||
| 367 | } | ||
| 368 | |||
| 369 | let parsed: unknown; | ||
| 370 | try { | ||
| 371 | parsed = JSON.parse(trimmed); | ||
| 372 | } catch { | ||
| 373 | throw new Error(`${label} must be valid JSON.`); | ||
| 374 | } | ||
| 375 | |||
| 376 | if ( | ||
| 377 | !Array.isArray(parsed) || | ||
| 378 | !parsed.every((value) => typeof value === "string") | ||
| 379 | ) { | ||
| 380 | throw new Error(`${label} must be a JSON array of strings.`); | ||
| 381 | } | ||
| 382 | |||
| 383 | const normalized = parsed | ||
| 384 | .map((value) => value.trim()) | ||
| 385 | .filter((value) => value.length > 0); | ||
| 386 | |||
| 387 | return normalized.length > 0 ? normalized : null; | ||
| 388 | }; | ||
| 389 | |||
| 390 | const buildSyntheticMcpStdioEndpoint = (input: { | 361 | const buildSyntheticMcpStdioEndpoint = (input: { |
| 391 | name?: string | null; | 362 | name?: string | null; |
| 392 | endpoint?: string | null; | 363 | endpoint?: string | null; |
| @@ -507,38 +478,6 @@ const formStateFromSource = (source: Source): SourceFormState => ({ · 114 unmodified lines | |||
| 507 | : {}), | 478 | : {}), |
| 508 | }); | 479 | }); |
| 509 | 480 | ||
| 510 | const parseJsonStringMap = ( | ||
| 511 | label: string, | ||
| 512 | text: string, | ||
| 513 | ): Record<string, string> | null => { | ||
| 514 | const trimmed = text.trim(); | ||
| 515 | if (trimmed.length === 0) { | ||
| 516 | return null; | ||
| 517 | } | ||
| 518 | |||
| 519 | let parsed: unknown; | ||
| 520 | try { | ||
| 521 | parsed = JSON.parse(trimmed); | ||
| 522 | } catch { | ||
| 523 | throw new Error(`${label} must be valid JSON.`); | ||
| 524 | } | ||
| 525 | |||
| 526 | if (parsed === null || Array.isArray(parsed) || typeof parsed !== "object") { | ||
| 527 | throw new Error(`${label} must be a JSON object with string values.`); | ||
| 528 | } | ||
| 529 | |||
| 530 | const entries = Object.entries(parsed as Record<string, unknown>); | ||
| 531 | const normalized: Record<string, string> = {}; | ||
| 532 | for (const [key, value] of entries) { | ||
| 533 | if (typeof value !== "string") { | ||
| 534 | throw new Error(`${label} must only contain string values.`); | ||
| 535 | } | ||
| 536 | normalized[key] = value; | ||
| 537 | } | ||
| 538 | |||
| 539 | return Object.keys(normalized).length === 0 ? null : normalized; | ||
| 540 | }; | ||
| 541 | |||
| 542 | const buildAuthPayload = ( | 481 | const buildAuthPayload = ( |
| 543 | state: SourceFormState, | 482 | state: SourceFormState, |
| 544 | ): CreateSourcePayload["auth"] => { | 483 | ): CreateSourcePayload["auth"] => { |
| @@ -193,31 +193,40 @@ export const createLiveExecutionManager = () => { · 192 unmodified lines | |||
| 193 | response, | 193 | response, |
| 194 | }; | 194 | }; |
| 195 | 195 | ||
| 196 | yield* publishState({ | 196 | return yield* Effect.gen(function* () { |
| 197 | executionId, | 197 | yield* publishState({ |
| 198 | state: "waiting_for_interaction", | 198 | executionId, |
| 199 | }); | 199 | state: "waiting_for_interaction", |
| 200 | 200 | }); | |
| 201 | const resolved = yield* Deferred.await(response); | 201 | |
| 202 | const resolvedAt = Date.now(); | 202 | const resolved = yield* Deferred.await(response); |
| 203 | 203 | const resolvedAt = Date.now(); | |
| 204 | yield* rows.executionInteractions.update(interaction.id, { | 204 | |
| 205 | status: resolved.action === "cancel" ? "cancelled" : "resolved", | 205 | yield* rows.executionInteractions.update(interaction.id, { |
| 206 | responseJson: serializeJson(sanitizePersistedElicitationResponse(resolved)), | 206 | status: resolved.action === "cancel" ? "cancelled" : "resolved", |
| 207 | responsePrivateJson: serializeJson(resolved), | 207 | responseJson: serializeJson(sanitizePersistedElicitationResponse(resolved)), |
| 208 | updatedAt: resolvedAt, | 208 | responsePrivateJson: serializeJson(resolved), |
| 209 | }); | 209 | updatedAt: resolvedAt, |
| 210 | yield* rows.executions.update(executionId, { | 210 | }); |
| 211 | status: "running", | 211 | yield* rows.executions.update(executionId, { |
| 212 | updatedAt: resolvedAt, | 212 | status: "running", |
| 213 | }); | 213 | updatedAt: resolvedAt, |
| 214 | yield* publishState({ | 214 | }); |
| 215 | executionId, | 215 | yield* publishState({ |
| 216 | state: "running", | 216 | executionId, |
| 217 | }); | 217 | state: "running", |
| 218 | 218 | }); | |
| 219 | run.currentInteraction = null; | 219 | |
| 220 | return resolved; | 220 | return resolved; |
| 221 | }).pipe( | ||
| 222 | Effect.ensuring( | ||
| 223 | Effect.sync(() => { | ||
| 224 | if (run.currentInteraction?.interactionId === interaction.id) { | ||
| 225 | run.currentInteraction = null; | ||
| 226 | } | ||
| 227 | }), | ||
| 228 | ), | ||
| 229 | ); | ||
| 221 | }), | 230 | }), |
| 222 | 231 | ||
| 223 | resolveInteraction: ({ executionId, response }) => | 232 | resolveInteraction: ({ executionId, response }) => |
| @@ -234,20 +243,21 @@ export const createLiveExecutionManager = () => { · 10 unmodified lines | |||
| 234 | 243 | ||
| 235 | finishRun: ({ executionId, state }) => | 244 | finishRun: ({ executionId, state }) => |
| 236 | publishState({ executionId, state }).pipe( | 245 | publishState({ executionId, state }).pipe( |
| 237 | Effect.zipRight( | 246 | Effect.zipRight(clearMcpConnectionPoolRun(executionId)), |
| 238 | clearMcpConnectionPoolRun(executionId).pipe( | 247 | Effect.ensuring( |
| 239 | Effect.zipRight(Effect.sync(() => { | 248 | Effect.sync(() => { |
| 240 | runs.delete(executionId); | 249 | runs.delete(executionId); |
| 241 | })), | 250 | }), |
| 242 | ), | ||
| 243 | ), | 251 | ), |
| 244 | ), | 252 | ), |
| 245 | 253 | ||
| 246 | clearRun: (executionId) => | 254 | clearRun: (executionId) => |
| 247 | clearMcpConnectionPoolRun(executionId).pipe( | 255 | clearMcpConnectionPoolRun(executionId).pipe( |
| 248 | Effect.zipRight(Effect.sync(() => { | 256 | Effect.ensuring( |
| 249 | runs.delete(executionId); | 257 | Effect.sync(() => { |
| 250 | })), | 258 | runs.delete(executionId); |
| 259 | }), | ||
| 260 | ), | ||
| 251 | ), | 261 | ), |
| 252 | } satisfies LiveExecutionManagerShape; | 262 | } satisfies LiveExecutionManagerShape; |
| 253 | 263 | ||