@codemation/host 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (223) hide show
  1. package/CHANGELOG.md +431 -0
  2. package/LICENSE +1 -37
  3. package/dist/{ApiPaths-CLTHphYZ.js → ApiPaths-Dv1dcHu_.js} +4 -4
  4. package/dist/ApiPaths-Dv1dcHu_.js.map +1 -0
  5. package/dist/{AppConfigFactory-C6q-CSKb.js → AppConfigFactory-Cx4qQvRk.js} +112 -52
  6. package/dist/AppConfigFactory-Cx4qQvRk.js.map +1 -0
  7. package/dist/{AppConfigFactory-YnveXE9k.d.ts → AppConfigFactory-DnLoQ9Li.d.ts} +8490 -5548
  8. package/dist/{AppContainerFactory-qaqc-R1D.js → AppContainerFactory-DqKYCRNP.js} +7641 -2083
  9. package/dist/AppContainerFactory-DqKYCRNP.js.map +1 -0
  10. package/dist/{CodemationAppContext-DRu1Dpri.d.ts → CodemationAppContext-CKVv9W9q.d.ts} +8 -4
  11. package/dist/{CodemationAuthoring.types-fBRppnmi.d.ts → CodemationAuthoring.types-DA3G3s6d.d.ts} +25 -5
  12. package/dist/{CodemationAuthoring.types-DZl-sJaM.js → CodemationAuthoring.types-NGkBcmmT.js} +18 -6
  13. package/dist/CodemationAuthoring.types-NGkBcmmT.js.map +1 -0
  14. package/dist/{CodemationConfigNormalizer-DVko3cVN.d.ts → CodemationConfigNormalizer-BAKjetJ6.d.ts} +3 -3
  15. package/dist/{CodemationConsumerConfigLoader-BeAUS144.js → CodemationConsumerConfigLoader-GYpBBvqE.js} +79 -10
  16. package/dist/CodemationConsumerConfigLoader-GYpBBvqE.js.map +1 -0
  17. package/dist/{CodemationConsumerConfigLoader-DJWr86f-.d.ts → CodemationConsumerConfigLoader-nxOqvv46.d.ts} +17 -2
  18. package/dist/{CodemationPluginListMerger-B-W5Fa_X.js → CodemationPluginListMerger-D1B1IEbt.js} +1 -1
  19. package/dist/{CodemationPluginListMerger-B-W5Fa_X.js.map → CodemationPluginListMerger-D1B1IEbt.js.map} +1 -1
  20. package/dist/{CodemationPluginListMerger-DGc-jfa2.d.ts → CodemationPluginListMerger-DKLAHT2b.d.ts} +123 -16
  21. package/dist/CodemationTsyringeTypeInfoRegistrar-Bj6FJYFz.js +97 -0
  22. package/dist/CodemationTsyringeTypeInfoRegistrar-Bj6FJYFz.js.map +1 -0
  23. package/dist/{CodemationWhitelabelConfig-CWbcyQqn.d.ts → CodemationWhitelabelConfig-Ca2mCUeC.d.ts} +2 -2
  24. package/dist/{CollectionContracts.types-DdpHft0i.d.ts → CollectionContracts.types-DDyFYT_D.d.ts} +1 -1
  25. package/dist/{CredentialContractsRegistry-DrMIDSw8.d.ts → CredentialContractsRegistry-Bq2bq28t.d.ts} +2 -2
  26. package/dist/{CredentialServices-UfvHB-rN.d.ts → CredentialServices-Be2I60Th.d.ts} +65 -20
  27. package/dist/{CredentialServices-CgxwguAv.js → CredentialServices-Dk8yypeL.js} +310 -51
  28. package/dist/CredentialServices-Dk8yypeL.js.map +1 -0
  29. package/dist/InternalHonoApiRouteRegistrar-Ce1yxpnO.d.ts +17 -0
  30. package/dist/InternalPingRegistrar-DY3kSfxP.js +221 -0
  31. package/dist/InternalPingRegistrar-DY3kSfxP.js.map +1 -0
  32. package/dist/{ItemsInputNormalizer-C-KHg9Mo.d.ts → ItemsInputNormalizer-_RwIfRIQ.d.ts} +89 -25
  33. package/dist/{LogLevelPolicyFactory-CampWO0l.d.ts → LogLevelPolicyFactory-ewCHLDLn.d.ts} +2 -2
  34. package/dist/{PublicFrontendBootstrap-DzBgwOnG.d.ts → PublicFrontendBootstrap-Cev3qK46.d.ts} +9 -2
  35. package/dist/PublicFrontendBootstrapFactory-CY2FS-5g.d.ts +82 -0
  36. package/dist/{PublicFrontendBootstrapJsonCodec-Cl_DLRh0.d.ts → PublicFrontendBootstrapJsonCodec-CXG9Dxft.d.ts} +3 -3
  37. package/dist/{PublicFrontendBootstrapJsonCodec-DzqvA0uo.js → PublicFrontendBootstrapJsonCodec-CegIF_ne.js} +7 -2
  38. package/dist/PublicFrontendBootstrapJsonCodec-CegIF_ne.js.map +1 -0
  39. package/dist/ServerLoggerFactory-Ckk52S3w.js +223 -0
  40. package/dist/ServerLoggerFactory-Ckk52S3w.js.map +1 -0
  41. package/dist/{TelemetryContracts-DbaNomrH.d.ts → TelemetryContracts-BtDx84Cp.d.ts} +13 -4
  42. package/dist/{WorkflowPolicyUiPresentationFactory-DQEY-h_S.d.ts → WorkflowPolicyUiPresentationFactory-6MyjCvBO.d.ts} +2 -2
  43. package/dist/{WorkflowPolicyUiPresentationFactory-DhPqQ9aB.js → WorkflowPolicyUiPresentationFactory-Bb-ae_Zh.js} +1 -1
  44. package/dist/{WorkflowPolicyUiPresentationFactory-DhPqQ9aB.js.map → WorkflowPolicyUiPresentationFactory-Bb-ae_Zh.js.map} +1 -1
  45. package/dist/{WorkflowViewContracts-CzK2KFuz.d.ts → WorkflowViewContracts-B7aFQcIw.d.ts} +10 -1
  46. package/dist/authoring.d.ts +5 -5
  47. package/dist/authoring.js +1 -1
  48. package/dist/client.d.ts +4 -4
  49. package/dist/client.js +2 -2
  50. package/dist/consumer.d.ts +6 -6
  51. package/dist/consumer.js +2 -2
  52. package/dist/credentials.d.ts +6 -6
  53. package/dist/credentials.js +1 -1
  54. package/dist/devServerSidecar.d.ts +2 -2
  55. package/dist/devServerSidecar.js +1 -94
  56. package/dist/devServerSidecar.js.map +1 -1
  57. package/dist/dto.d.ts +6 -6
  58. package/dist/{index-BbBk26m0.d.ts → index-DilAYwnH.d.ts} +49 -3
  59. package/dist/index.d.ts +110 -21
  60. package/dist/index.js +15 -13
  61. package/dist/mapping.d.ts +2 -2
  62. package/dist/mapping.js +1 -1
  63. package/dist/nextServer.d.ts +43 -88
  64. package/dist/nextServer.js +9 -7
  65. package/dist/pairing.d.ts +93 -0
  66. package/dist/pairing.js +5 -0
  67. package/dist/pairing.types-snfZ_OzB.d.ts +19 -0
  68. package/dist/{persistenceServer-CmsIKnO9.js → persistenceServer-C-hH4z6l.js} +2 -2
  69. package/dist/{persistenceServer-CmsIKnO9.js.map → persistenceServer-C-hH4z6l.js.map} +1 -1
  70. package/dist/persistenceServer-CeTHtC6E.d.ts +30 -0
  71. package/dist/persistenceServer.d.ts +8 -8
  72. package/dist/persistenceServer.js +3 -3
  73. package/dist/{server-MUNGsBYK.d.ts → server-C4bS62rg.d.ts} +21 -6
  74. package/dist/{server-CJFfY67o.js → server-Y7kxwtCK.js} +7 -6
  75. package/dist/{server-CJFfY67o.js.map → server-Y7kxwtCK.js.map} +1 -1
  76. package/dist/server.d.ts +14 -14
  77. package/dist/server.js +13 -11
  78. package/package.json +29 -42
  79. package/prisma/migrations/20260519000000_workflow_audit_log/migration.sql +23 -0
  80. package/prisma/migrations/20260519100000_storage_growth_fixes/migration.sql +61 -0
  81. package/prisma/migrations.sqlite/20260519000000_workflow_audit_log/migration.sql +21 -0
  82. package/prisma/migrations.sqlite/20260519100000_storage_growth_fixes/migration.sql +29 -0
  83. package/prisma/schema.postgresql.prisma +55 -17
  84. package/prisma/schema.sqlite.prisma +55 -17
  85. package/prisma-generated/prisma-postgresql-client/edge.js +33 -5
  86. package/prisma-generated/prisma-postgresql-client/index-browser.js +29 -1
  87. package/prisma-generated/prisma-postgresql-client/index.d.ts +8933 -5716
  88. package/prisma-generated/prisma-postgresql-client/index.js +33 -5
  89. package/prisma-generated/prisma-postgresql-client/package.json +1 -1
  90. package/prisma-generated/prisma-postgresql-client/schema.prisma +38 -0
  91. package/prisma-generated/prisma-sqlite-client/edge.js +33 -5
  92. package/prisma-generated/prisma-sqlite-client/index-browser.js +29 -1
  93. package/prisma-generated/prisma-sqlite-client/index.d.ts +8925 -5713
  94. package/prisma-generated/prisma-sqlite-client/index.js +33 -5
  95. package/prisma-generated/prisma-sqlite-client/package.json +1 -1
  96. package/prisma-generated/prisma-sqlite-client/schema.prisma +38 -0
  97. package/scripts/check-collections.mjs +18 -0
  98. package/scripts/generate-prisma-clients.mjs +20 -11
  99. package/src/application/WorkflowAuditLogPruneScheduler.ts +96 -0
  100. package/src/application/auth/AuthenticatedPrincipal.ts +4 -0
  101. package/src/application/commands/StartWorkflowRunCommandHandler.ts +4 -0
  102. package/src/application/contracts/WorkflowViewContracts.ts +6 -0
  103. package/src/application/contracts/WorkflowWebsocketMessage.ts +3 -1
  104. package/src/application/mapping/WorkflowDefinitionMapper.ts +40 -1
  105. package/src/application/runs/WorkflowRunRetentionPruneScheduler.ts +7 -1
  106. package/src/application/telemetry/OtelExecutionTelemetry.types.ts +5 -0
  107. package/src/application/telemetry/OtelExecutionTelemetryFactory.ts +4 -0
  108. package/src/application/telemetry/StoredTelemetrySpanScope.ts +6 -2
  109. package/src/application/telemetry/TelemetryRetentionTimestampFactory.ts +27 -17
  110. package/src/application/telemetry/TelemetrySpanPublisher.ts +11 -0
  111. package/src/application/websocket/TelemetrySpanWebsocketRelay.ts +31 -0
  112. package/src/applicationTokens.ts +20 -1
  113. package/src/audit/IAuditEmitter.ts +32 -0
  114. package/src/audit/PrismaWorkflowAuditLogRepository.ts +34 -0
  115. package/src/audit/WorkflowAuditLogWriter.ts +125 -0
  116. package/src/auth/managed/ManagedAuthConfig.ts +29 -0
  117. package/src/auth/managed/ManagedAuthMiddleware.ts +52 -0
  118. package/src/auth/managed/ManagedCorsMiddleware.ts +43 -0
  119. package/src/auth/managed/ManagedModeBootGuard.ts +27 -0
  120. package/src/auth/managed/index.ts +5 -0
  121. package/src/bootstrap/AppContainerFactory.ts +277 -29
  122. package/src/bootstrap/AppContainerLifecycle.ts +31 -0
  123. package/src/bootstrap/perf/BootTimer.ts +168 -0
  124. package/src/bootstrap/runtime/AppConfigFactory.ts +21 -65
  125. package/src/bootstrap/runtime/FrontendRuntime.ts +4 -1
  126. package/src/bootstrap/runtime/WorkerRuntime.ts +2 -1
  127. package/src/credentials/BrokerClient.ts +49 -0
  128. package/src/credentials/BrokerRefreshError.ts +12 -0
  129. package/src/credentials/BrokerRefreshInvalidGrantError.ts +13 -0
  130. package/src/credentials/ControlPlaneCatalogFetcher.ts +261 -0
  131. package/src/credentials/CredentialOAuth2MaterialReader.ts +136 -0
  132. package/src/credentials/InternalCredentialsListRegistrar.ts +48 -0
  133. package/src/credentials/InternalCredentialsPushRegistrar.ts +125 -0
  134. package/src/credentials/LocalOAuthFlowExecutor.ts +316 -0
  135. package/src/credentials/ManagedOAuthFlowExecutor.ts +94 -0
  136. package/src/credentials/ManagedOAuthRefreshInvalidGrantError.ts +13 -0
  137. package/src/credentials/catalogTypes.ts +4 -0
  138. package/src/credentials/refresh/CredentialDisconnectedError.ts +11 -0
  139. package/src/domain/credentials/CredentialBindingService.ts +54 -2
  140. package/src/domain/credentials/CredentialKeyRotatedError.ts +22 -0
  141. package/src/domain/credentials/CredentialSecretCipher.ts +68 -6
  142. package/src/domain/credentials/CredentialTypeRegistryImpl.ts +117 -10
  143. package/src/domain/credentials/OAuth2RedirectUriResolver.ts +79 -0
  144. package/src/domain/credentials/WorkflowCredentialNodeResolver.ts +14 -5
  145. package/src/domain/telemetry/TelemetryContracts.ts +7 -1
  146. package/src/domain/workflows/WorkflowActivationPreflight.ts +24 -1
  147. package/src/domain/workflows/WorkflowActivationPreflightRules.ts +40 -1
  148. package/src/index.ts +6 -0
  149. package/src/infrastructure/binary/LocalFilesystemBinaryStorageRegistry.ts +29 -1
  150. package/src/infrastructure/binary/S3BinaryStorage.ts +169 -0
  151. package/src/infrastructure/binary/S3BinaryStorageConfig.ts +17 -0
  152. package/src/infrastructure/config/CodemationPluginRegistrar.ts +3 -1
  153. package/src/infrastructure/persistence/CodemationDatabaseUrlParser.ts +41 -0
  154. package/src/infrastructure/persistence/InMemoryTelemetryArtifactStore.ts +8 -3
  155. package/src/infrastructure/persistence/PrismaMigrationDeployer.ts +21 -13
  156. package/src/infrastructure/persistence/PrismaTelemetryArtifactStore.ts +43 -8
  157. package/src/infrastructure/persistence/PrismaWorkflowRunRepository.ts +26 -3
  158. package/src/infrastructure/persistence/PrismaWorkflowSnapshotRepository.ts +48 -0
  159. package/src/mcp/AgentMcpIntegrationImpl.ts +344 -0
  160. package/src/mcp/McpClientFactory.ts +29 -0
  161. package/src/mcp/McpConnectionPool.ts +184 -0
  162. package/src/mcp/McpConnectionPool.types.ts +12 -0
  163. package/src/mcp/McpServerCatalog.ts +104 -0
  164. package/src/mcp/index.ts +5 -0
  165. package/src/pairing/HmacRequestSigner.ts +32 -0
  166. package/src/pairing/IncomingHmacVerifier.ts +82 -0
  167. package/src/pairing/InternalHmacAuthMiddleware.ts +33 -0
  168. package/src/pairing/InternalPingRegistrar.ts +25 -0
  169. package/src/pairing/PairedFetch.ts +33 -0
  170. package/src/pairing/PairingConfigFactory.ts +35 -0
  171. package/src/pairing/PairingConfigToken.ts +6 -0
  172. package/src/pairing/index.ts +14 -0
  173. package/src/pairing/pairing.types.ts +18 -0
  174. package/src/pairing.ts +17 -0
  175. package/src/persistenceServer.ts +1 -0
  176. package/src/presentation/config/AppConfig.ts +7 -1
  177. package/src/presentation/config/CodemationAuthConfig.ts +1 -1
  178. package/src/presentation/config/CodemationAuthoring.types.ts +54 -5
  179. package/src/presentation/config/CodemationConfig.ts +3 -0
  180. package/src/presentation/config/CodemationConfigNormalizer.ts +39 -1
  181. package/src/presentation/config/CodemationPlugin.ts +2 -1
  182. package/src/presentation/frontend/CodemationFrontendAuthSnapshot.ts +5 -0
  183. package/src/presentation/frontend/CodemationFrontendAuthSnapshotFactory.ts +7 -1
  184. package/src/presentation/frontend/PublicFrontendBootstrap.ts +2 -0
  185. package/src/presentation/frontend/PublicFrontendBootstrapFactory.ts +5 -1
  186. package/src/presentation/frontend/PublicFrontendBootstrapJsonCodec.ts +4 -1
  187. package/src/presentation/http/ApiPaths.ts +4 -4
  188. package/src/presentation/http/ServerHttpErrorResponseFactory.ts +39 -2
  189. package/src/presentation/http/hono/CodemationHonoApiAppFactory.ts +33 -8
  190. package/src/presentation/http/hono/InternalHonoApiRouteRegistrar.ts +12 -0
  191. package/src/presentation/http/hono/registrars/ManagedMeHonoApiRouteRegistrar.ts +35 -0
  192. package/src/presentation/http/hono/registrars/OAuth2HonoApiRouteRegistrar.ts +2 -2
  193. package/src/presentation/http/routeHandlers/CredentialHttpRouteHandler.ts +28 -0
  194. package/src/presentation/http/routeHandlers/OAuth2HttpRouteHandlerFactory.ts +98 -41
  195. package/src/presentation/server/CodemationConsumerConfigLoader.ts +54 -7
  196. package/src/presentation/server/CodemationPluginDiscovery.ts +5 -0
  197. package/src/presentation/server/WorkflowDefinitionExportsResolver.ts +18 -0
  198. package/src/presentation/server/WorkflowModulePathFinder.ts +12 -1
  199. package/src/presentation/websocket/ManagedWebsocketAuthenticator.ts +50 -0
  200. package/src/presentation/websocket/WebsocketAuthenticator.types.ts +12 -0
  201. package/src/presentation/websocket/WorkflowWebsocketServer.ts +24 -3
  202. package/src/process/ExecaProcessRunner.ts +41 -0
  203. package/src/process/ProcessRunner.types.ts +39 -0
  204. package/src/server.ts +2 -0
  205. package/src/workflows/InternalWorkflowActivationRegistrar.ts +42 -0
  206. package/src/workflows/InternalWorkflowDetailRegistrar.ts +33 -0
  207. package/src/workflows/InternalWorkflowTestRunRegistrar.ts +91 -0
  208. package/src/workflows/InternalWorkflowsListRegistrar.ts +28 -0
  209. package/src/workflows/discovery/WorkflowDirectoryDiscoverer.ts +79 -0
  210. package/tsconfig.json +2 -0
  211. package/vitest.shared.ts +5 -0
  212. package/dist/ApiPaths-CLTHphYZ.js.map +0 -1
  213. package/dist/AppConfigFactory-C6q-CSKb.js.map +0 -1
  214. package/dist/AppContainerFactory-qaqc-R1D.js.map +0 -1
  215. package/dist/CodemationAuthoring.types-DZl-sJaM.js.map +0 -1
  216. package/dist/CodemationConsumerConfigLoader-BeAUS144.js.map +0 -1
  217. package/dist/CredentialServices-CgxwguAv.js.map +0 -1
  218. package/dist/PublicFrontendBootstrapFactory-Cb2pLmDd.d.ts +0 -45
  219. package/dist/PublicFrontendBootstrapJsonCodec-DzqvA0uo.js.map +0 -1
  220. package/dist/ServerLoggerFactory-BKSIh9Xv.js +0 -98
  221. package/dist/ServerLoggerFactory-BKSIh9Xv.js.map +0 -1
  222. package/dist/persistenceServer-vtJAGDat.d.ts +0 -9
  223. package/src/domain/credentials/OAuth2ConnectServiceFactory.ts +0 -411
@@ -22,6 +22,8 @@ import { inject, injectable } from "@codemation/core";
22
22
  import type { WorkflowRunRepository } from "../../domain/runs/WorkflowRunRepository";
23
23
  import type { Prisma } from "../../../prisma-generated/prisma-postgresql-client/client.js";
24
24
  import { PrismaDatabaseClientToken, type PrismaDatabaseClient } from "./PrismaDatabaseClient";
25
+ import type { WorkflowSnapshotRepository } from "./PrismaWorkflowSnapshotRepository";
26
+ import { PrismaWorkflowSnapshotRepository } from "./PrismaWorkflowSnapshotRepository";
25
27
 
26
28
  type ExecutionInstanceRow = {
27
29
  instanceId: string;
@@ -80,7 +82,10 @@ type RunSlotProjectionRow = {
80
82
 
81
83
  @injectable()
82
84
  export class PrismaWorkflowRunRepository implements WorkflowRunRepository, WorkflowExecutionRepository {
83
- constructor(@inject(PrismaDatabaseClientToken) private readonly prisma: PrismaDatabaseClient) {}
85
+ constructor(
86
+ @inject(PrismaDatabaseClientToken) private readonly prisma: PrismaDatabaseClient,
87
+ @inject(PrismaWorkflowSnapshotRepository) private readonly snapshotRepo: WorkflowSnapshotRepository,
88
+ ) {}
84
89
 
85
90
  async createRun(args: {
86
91
  runId: RunId;
@@ -96,6 +101,14 @@ export class PrismaWorkflowRunRepository implements WorkflowRunRepository, Workf
96
101
  }): Promise<void> {
97
102
  const now = new Date().toISOString();
98
103
  const testContext = args.executionOptions?.testContext;
104
+ const snapshotJson = args.workflowSnapshot ? JSON.stringify(args.workflowSnapshot) : null;
105
+ const workflowSnapshotId =
106
+ snapshotJson !== null
107
+ ? await this.snapshotRepo.findOrCreate({
108
+ workflowId: args.workflowId,
109
+ snapshotJson,
110
+ })
111
+ : null;
99
112
  await this.prisma.run.create({
100
113
  data: {
101
114
  runId: args.runId,
@@ -108,7 +121,8 @@ export class PrismaWorkflowRunRepository implements WorkflowRunRepository, Workf
108
121
  revision: 0,
109
122
  outputsByNodeJson: JSON.stringify({}),
110
123
  controlJson: args.control ? JSON.stringify(args.control) : null,
111
- workflowSnapshotJson: args.workflowSnapshot ? JSON.stringify(args.workflowSnapshot) : null,
124
+ workflowSnapshotJson: snapshotJson,
125
+ workflowSnapshotId,
112
126
  policySnapshotJson: args.policySnapshot ? JSON.stringify(args.policySnapshot) : null,
113
127
  engineCountersJson: args.engineCounters ? JSON.stringify(args.engineCounters) : null,
114
128
  mutableStateJson: args.mutableState ? JSON.stringify(args.mutableState) : null,
@@ -290,6 +304,14 @@ export class PrismaWorkflowRunRepository implements WorkflowRunRepository, Workf
290
304
  const workItems = this.buildWorkItems(state, now);
291
305
  const instances = this.buildExecutionInstances(state);
292
306
  const projectionJson = this.buildProjectionSlotStatesJson(state);
307
+ const snapshotJson = state.workflowSnapshot ? JSON.stringify(state.workflowSnapshot) : null;
308
+ const workflowSnapshotId =
309
+ snapshotJson !== null
310
+ ? await this.snapshotRepo.findOrCreate({
311
+ workflowId: state.workflowId,
312
+ snapshotJson,
313
+ })
314
+ : null;
293
315
 
294
316
  await this.prisma.$transaction(async (tx) => {
295
317
  await tx.runWorkItem.deleteMany({ where: { runId: state.runId } });
@@ -389,7 +411,8 @@ export class PrismaWorkflowRunRepository implements WorkflowRunRepository, Workf
389
411
  parentJson: state.parent ? JSON.stringify(state.parent) : null,
390
412
  executionOptionsJson: state.executionOptions ? JSON.stringify(state.executionOptions) : null,
391
413
  controlJson: state.control ? JSON.stringify(state.control) : null,
392
- workflowSnapshotJson: state.workflowSnapshot ? JSON.stringify(state.workflowSnapshot) : null,
414
+ workflowSnapshotJson: snapshotJson,
415
+ workflowSnapshotId,
393
416
  policySnapshotJson: state.policySnapshot ? JSON.stringify(state.policySnapshot) : null,
394
417
  engineCountersJson: state.engineCounters ? JSON.stringify(state.engineCounters) : null,
395
418
  mutableStateJson: state.mutableState ? JSON.stringify(state.mutableState) : null,
@@ -0,0 +1,48 @@
1
+ import { createHash } from "node:crypto";
2
+ import { inject, injectable } from "@codemation/core";
3
+ import { PrismaDatabaseClientToken, type PrismaDatabaseClient } from "./PrismaDatabaseClient";
4
+
5
+ export interface WorkflowSnapshotRepository {
6
+ /**
7
+ * Returns the id of an existing snapshot matching (workflowId, snapshotHash), or creates
8
+ * a new one from the provided snapshotJson. Deduplication is by content hash.
9
+ */
10
+ findOrCreate(args: Readonly<{ workflowId: string; snapshotJson: string }>): Promise<string>;
11
+ }
12
+
13
+ @injectable()
14
+ export class PrismaWorkflowSnapshotRepository implements WorkflowSnapshotRepository {
15
+ constructor(
16
+ @inject(PrismaDatabaseClientToken)
17
+ private readonly prisma: PrismaDatabaseClient,
18
+ ) {}
19
+
20
+ async findOrCreate(args: Readonly<{ workflowId: string; snapshotJson: string }>): Promise<string> {
21
+ const snapshotHash = createHash("sha256").update(args.snapshotJson, "utf8").digest("hex");
22
+ const existing = await this.prisma.workflowSnapshot.findUnique({
23
+ where: { workflowId_snapshotHash: { workflowId: args.workflowId, snapshotHash } },
24
+ select: { id: true },
25
+ });
26
+ if (existing) {
27
+ return existing.id;
28
+ }
29
+ const id = crypto.randomUUID();
30
+ await this.prisma.workflowSnapshot.upsert({
31
+ where: { workflowId_snapshotHash: { workflowId: args.workflowId, snapshotHash } },
32
+ create: {
33
+ id,
34
+ workflowId: args.workflowId,
35
+ snapshotHash,
36
+ snapshotJson: args.snapshotJson,
37
+ createdAt: new Date().toISOString(),
38
+ },
39
+ update: {},
40
+ });
41
+ // Re-fetch so the returned id is the winner under concurrent inserts
42
+ const row = await this.prisma.workflowSnapshot.findUniqueOrThrow({
43
+ where: { workflowId_snapshotHash: { workflowId: args.workflowId, snapshotHash } },
44
+ select: { id: true },
45
+ });
46
+ return row.id;
47
+ }
48
+ }
@@ -0,0 +1,344 @@
1
+ import type { ToolSet } from "ai";
2
+ import {
3
+ AgentBindError,
4
+ CodemationTelemetryAttributeNames,
5
+ ConnectionInvocationIdFactory,
6
+ ConnectionNodeIdFactory,
7
+ inject,
8
+ injectable,
9
+ type AgentMcpIntegration,
10
+ type AgentMcpToolMap,
11
+ type ConnectionInvocationAppendArgs,
12
+ type JsonValue,
13
+ type McpServerDeclaration,
14
+ type NeedsReconsentEvent,
15
+ type NodeActivationId,
16
+ type NodeIterationId,
17
+ type ConnectionInvocationId,
18
+ type TelemetrySpanEventRecord,
19
+ } from "@codemation/core";
20
+ import { ApplicationTokens } from "../applicationTokens";
21
+ import type { LoggerFactory } from "../application/logging/Logger";
22
+ import { McpServerCatalog } from "./McpServerCatalog";
23
+ import { McpConnectionPool } from "./McpConnectionPool";
24
+ import type { CredentialStore } from "../domain/credentials/CredentialServices";
25
+
26
+ /**
27
+ * Host-side implementation of AgentMcpIntegration.
28
+ *
29
+ * Resolves the credential binding for each declared MCP server via the standard
30
+ * credential-binding table — the binding lives on the MCP connection node itself
31
+ * (slot key `"credential"`), matching ChatModel/Tool connection nodes. Opens pool
32
+ * connections and returns a ToolSet map with execute callbacks wrapped for
33
+ * telemetry + 403 detection.
34
+ */
35
+ @injectable()
36
+ export class AgentMcpIntegrationImpl implements AgentMcpIntegration {
37
+ constructor(
38
+ @inject(McpServerCatalog) private readonly catalog: McpServerCatalog,
39
+ @inject(McpConnectionPool) private readonly pool: McpConnectionPool,
40
+ @inject(ApplicationTokens.CredentialStore) private readonly credentialStore: CredentialStore,
41
+ @inject(ApplicationTokens.LoggerFactory) private readonly loggers: LoggerFactory,
42
+ ) {}
43
+
44
+ async prepareMcpTools(args: Parameters<AgentMcpIntegration["prepareMcpTools"]>[0]): Promise<AgentMcpToolMap> {
45
+ const {
46
+ workflowId,
47
+ agentNodeId,
48
+ serverIds,
49
+ pinnedMcpTools: _pinnedMcpTools,
50
+ emitSpanEvent,
51
+ startChildSpan,
52
+ appendMcpInvocation,
53
+ parentAgentActivationId,
54
+ iterationId,
55
+ itemIndex,
56
+ parentInvocationId,
57
+ } = args;
58
+
59
+ const result = new Map<string, Readonly<Record<string, unknown>>>();
60
+ const logger = this.loggers.create("AgentMcpIntegrationImpl");
61
+
62
+ for (const serverId of serverIds) {
63
+ const decl = this.catalog.get(serverId);
64
+ if (!decl) {
65
+ throw new AgentBindError(`MCP server "${serverId}" not found in catalog`);
66
+ }
67
+
68
+ const credentialInstanceId = await this.resolveCredentialInstanceId(workflowId, agentNodeId, serverId);
69
+
70
+ // Validate scopes before opening the connection.
71
+ await this.validateScopes(decl, credentialInstanceId);
72
+
73
+ // Lazy-open via pool (single-flight, cached after first open).
74
+ await this.pool.getClient(credentialInstanceId, serverId);
75
+
76
+ // Fetch tool list from pool (cached after first fetch).
77
+ const rawTools = await this.pool.getTools(credentialInstanceId, serverId);
78
+
79
+ // Wrap each tool's execute for telemetry and 403 detection.
80
+ const wrappedTools = this.wrapToolExecutes({
81
+ tools: rawTools as ToolSet,
82
+ serverId,
83
+ credentialInstanceId,
84
+ agentNodeId,
85
+ emitSpanEvent,
86
+ startChildSpan,
87
+ logger,
88
+ appendMcpInvocation,
89
+ parentAgentActivationId,
90
+ iterationId,
91
+ itemIndex,
92
+ parentInvocationId,
93
+ });
94
+
95
+ result.set(serverId, wrappedTools as unknown as Readonly<Record<string, unknown>>);
96
+ }
97
+
98
+ return result;
99
+ }
100
+
101
+ /**
102
+ * Looks up the credential binding for the MCP connection node and verifies the
103
+ * referenced credential instance still exists.
104
+ */
105
+ private async resolveCredentialInstanceId(workflowId: string, agentNodeId: string, serverId: string): Promise<string> {
106
+ const mcpNodeId = ConnectionNodeIdFactory.mcpConnectionNodeId(agentNodeId, serverId);
107
+ const binding = await this.credentialStore.getBinding({ workflowId, nodeId: mcpNodeId, slotKey: "credential" });
108
+ if (!binding) {
109
+ throw new AgentBindError(
110
+ `MCP server "${serverId}" has no credential bound on connection node "${mcpNodeId}". ` +
111
+ `Bind a credential instance via the canvas credential dropdown before activation.`,
112
+ );
113
+ }
114
+ const instance = await this.credentialStore.getInstance(binding.instanceId);
115
+ if (!instance) {
116
+ throw new AgentBindError(
117
+ `Credential instance "${binding.instanceId}" not found for mcpServer "${serverId}" (connection node "${mcpNodeId}")`,
118
+ );
119
+ }
120
+ return instance.instanceId;
121
+ }
122
+
123
+ /**
124
+ * Validates that the credential instance's granted scopes cover the server's requiredScopes.
125
+ * Scopes are read from the OAuth2 material record (populated by the broker push endpoint).
126
+ */
127
+ private async validateScopes(decl: McpServerDeclaration, credentialInstanceId: string): Promise<void> {
128
+ if (!decl.requiredScopes?.length) {
129
+ return;
130
+ }
131
+
132
+ const material = await this.credentialStore.getOAuth2Material(credentialInstanceId);
133
+ const grantedScopes = new Set(material?.scopes ?? []);
134
+ const missing = decl.requiredScopes.filter((s) => !grantedScopes.has(s));
135
+
136
+ if (missing.length > 0) {
137
+ throw new AgentBindError(
138
+ `Credential instance "${credentialInstanceId}" lacks required scopes for server "${decl.id}": ${missing.join(", ")}. ` +
139
+ `Reconnect the credential to grant the missing scopes.`,
140
+ );
141
+ }
142
+ }
143
+
144
+ /**
145
+ * Returns a new ToolSet where each tool's execute callback is replaced with a wrapped version
146
+ * that:
147
+ * - Opens a child telemetry span tagged with mcp.server_id and mcp.tool_name.
148
+ * - Calls the original tool's execute (from @ai-sdk/mcp), which internally calls the MCP server.
149
+ * - On 403 / permission errors: emits a NeedsReconsentEvent span event, closes the span with
150
+ * error status, and re-throws a descriptive error. The agent turn continues for other tools.
151
+ */
152
+ private wrapToolExecutes(args: {
153
+ tools: ToolSet;
154
+ serverId: string;
155
+ credentialInstanceId: string;
156
+ agentNodeId: string;
157
+ emitSpanEvent: (event: TelemetrySpanEventRecord) => void;
158
+ startChildSpan: (args: { name: string; attributes?: Record<string, string> }) => {
159
+ end: (args?: { status?: "ok" | "error"; statusMessage?: string }) => void;
160
+ };
161
+ logger: ReturnType<LoggerFactory["create"]>;
162
+ appendMcpInvocation?: (args: ConnectionInvocationAppendArgs) => Promise<void>;
163
+ parentAgentActivationId?: NodeActivationId;
164
+ iterationId?: NodeIterationId;
165
+ itemIndex?: number;
166
+ parentInvocationId?: ConnectionInvocationId;
167
+ }): ToolSet {
168
+ const {
169
+ tools,
170
+ serverId,
171
+ credentialInstanceId,
172
+ agentNodeId,
173
+ emitSpanEvent,
174
+ startChildSpan,
175
+ logger,
176
+ appendMcpInvocation,
177
+ parentAgentActivationId,
178
+ iterationId,
179
+ itemIndex,
180
+ parentInvocationId,
181
+ } = args;
182
+ const wrapped: Record<string, ToolSet[string]> = {};
183
+ const checkPermissionError = (err: unknown): boolean => this.isPermissionError(err);
184
+ const connectionNodeId = ConnectionNodeIdFactory.mcpConnectionNodeId(agentNodeId, serverId);
185
+
186
+ for (const [toolName, toolDef] of Object.entries(tools)) {
187
+ const originalExecute = (toolDef as { execute?: (input: unknown) => Promise<unknown> }).execute;
188
+ const wrappedDef = {
189
+ ...toolDef,
190
+ execute: async (input: unknown): Promise<unknown> => {
191
+ const span = startChildSpan({
192
+ name: "mcp.tool_call",
193
+ attributes: {
194
+ [CodemationTelemetryAttributeNames.mcpServerId]: serverId,
195
+ [CodemationTelemetryAttributeNames.mcpToolName]: toolName,
196
+ },
197
+ });
198
+ const invocationId = ConnectionInvocationIdFactory.create();
199
+ const startedAtIso = new Date().toISOString();
200
+ const baseRecord = {
201
+ invocationId,
202
+ connectionNodeId,
203
+ parentAgentNodeId: agentNodeId,
204
+ parentAgentActivationId: parentAgentActivationId ?? agentNodeId,
205
+ iterationId,
206
+ itemIndex,
207
+ parentInvocationId,
208
+ subjectName: toolName,
209
+ };
210
+ const summarizedInput = this.summarizeForInvocation(input);
211
+ if (appendMcpInvocation) {
212
+ await appendMcpInvocation({
213
+ ...baseRecord,
214
+ status: "running",
215
+ managedInput: summarizedInput,
216
+ queuedAt: startedAtIso,
217
+ startedAt: startedAtIso,
218
+ statusLabel: `calling ${toolName}`,
219
+ });
220
+ }
221
+ try {
222
+ if (!originalExecute) {
223
+ throw new Error(`MCP tool "${toolName}" on server "${serverId}" has no execute callback`);
224
+ }
225
+ const result = await originalExecute(input);
226
+ span.end({ status: "ok" });
227
+ if (appendMcpInvocation) {
228
+ const finishedAtIso = new Date().toISOString();
229
+ await appendMcpInvocation({
230
+ ...baseRecord,
231
+ status: "completed",
232
+ managedInput: summarizedInput,
233
+ managedOutput: this.summarizeForInvocation(result),
234
+ queuedAt: startedAtIso,
235
+ startedAt: startedAtIso,
236
+ finishedAt: finishedAtIso,
237
+ });
238
+ }
239
+ return result;
240
+ } catch (error) {
241
+ if (checkPermissionError(error)) {
242
+ const event: NeedsReconsentEvent = {
243
+ serverId,
244
+ credentialInstanceId,
245
+ };
246
+ const spanEvent: TelemetrySpanEventRecord = {
247
+ name: "mcp.needs_reconsent",
248
+ attributes: {
249
+ "mcp.server_id": serverId,
250
+ "mcp.credential_instance_id": credentialInstanceId,
251
+ },
252
+ };
253
+ emitSpanEvent(spanEvent);
254
+ span.end({ status: "error", statusMessage: "MCP tool permission error" });
255
+ logger.warn(
256
+ `AgentMcpIntegrationImpl: permission error from MCP tool "${toolName}" on server "${serverId}". ` +
257
+ `NeedsReconsentEvent emitted for credential instance "${credentialInstanceId}".`,
258
+ error instanceof Error ? error : undefined,
259
+ );
260
+ const wrapped = new Error(
261
+ `MCP tool "${toolName}" on server "${serverId}" returned a permission error. ` +
262
+ `Reconnect the credential "${credentialInstanceId}" via the Connect flow. ` +
263
+ `needsReconsent: ${JSON.stringify(event satisfies NeedsReconsentEvent)}`,
264
+ { cause: error },
265
+ );
266
+ if (appendMcpInvocation) {
267
+ await appendMcpInvocation({
268
+ ...baseRecord,
269
+ status: "failed",
270
+ managedInput: summarizedInput,
271
+ error: { message: wrapped.message, name: wrapped.name },
272
+ queuedAt: startedAtIso,
273
+ startedAt: startedAtIso,
274
+ finishedAt: new Date().toISOString(),
275
+ });
276
+ }
277
+ // The event carries the structured data; the agent turn continues for other tools.
278
+ throw wrapped;
279
+ }
280
+ const effectiveMessage = error instanceof Error ? error.message : String(error);
281
+ span.end({
282
+ status: "error",
283
+ statusMessage: effectiveMessage,
284
+ });
285
+ if (appendMcpInvocation) {
286
+ await appendMcpInvocation({
287
+ ...baseRecord,
288
+ status: "failed",
289
+ managedInput: summarizedInput,
290
+ error: { message: effectiveMessage, name: error instanceof Error ? error.name : undefined },
291
+ queuedAt: startedAtIso,
292
+ startedAt: startedAtIso,
293
+ finishedAt: new Date().toISOString(),
294
+ });
295
+ }
296
+ throw error;
297
+ }
298
+ },
299
+ };
300
+ wrapped[toolName] = wrappedDef as unknown as ToolSet[string];
301
+ }
302
+
303
+ return wrapped as ToolSet;
304
+ }
305
+
306
+ private summarizeForInvocation(value: unknown): JsonValue | undefined {
307
+ if (value === undefined) return undefined;
308
+ try {
309
+ const serialized = JSON.stringify(value);
310
+ if (serialized.length > 1024) {
311
+ return { truncated: true, preview: serialized.slice(0, 1024) };
312
+ }
313
+ return JSON.parse(serialized) as JsonValue;
314
+ } catch {
315
+ return undefined;
316
+ }
317
+ }
318
+
319
+ /**
320
+ * Detects 403 / MCP-level permission / scope-insufficiency errors from callTool.
321
+ * The exact shape depends on how @ai-sdk/mcp surfaces them — we check for HTTP status
322
+ * 403, MCP error codes (insufficient_scope / UNAUTHORIZED), and common message patterns.
323
+ */
324
+ private isPermissionError(error: unknown): boolean {
325
+ if (!(error instanceof Error)) {
326
+ return false;
327
+ }
328
+ const msg = error.message.toLowerCase();
329
+ // HTTP 403 from the MCP transport
330
+ if (msg.includes("403") || msg.includes("forbidden")) {
331
+ return true;
332
+ }
333
+ // MCP-level error codes
334
+ if (msg.includes("insufficient_scope") || msg.includes("unauthorized") || msg.includes("unauthenticated")) {
335
+ return true;
336
+ }
337
+ // Check error name or code
338
+ const candidate = error as Error & { statusCode?: number; code?: string };
339
+ if (candidate.statusCode === 403 || candidate.code === "EUNAUTHORIZED") {
340
+ return true;
341
+ }
342
+ return false;
343
+ }
344
+ }
@@ -0,0 +1,29 @@
1
+ import { injectable } from "@codemation/core";
2
+ import { experimental_createMCPClient } from "@ai-sdk/mcp";
3
+ import type { MCPClient } from "@ai-sdk/mcp";
4
+
5
+ export type McpClientOpenArgs = Readonly<{
6
+ url: string;
7
+ headers: Record<string, string>;
8
+ }>;
9
+
10
+ export interface McpClientFactory {
11
+ open(args: McpClientOpenArgs): Promise<MCPClient>;
12
+ }
13
+
14
+ /**
15
+ * Default implementation — delegates to @ai-sdk/mcp's experimental_createMCPClient
16
+ * using the streamable HTTP transport.
17
+ */
18
+ @injectable()
19
+ export class DefaultMcpClientFactory implements McpClientFactory {
20
+ async open(args: McpClientOpenArgs): Promise<MCPClient> {
21
+ return experimental_createMCPClient({
22
+ transport: {
23
+ type: "http",
24
+ url: args.url,
25
+ headers: args.headers,
26
+ },
27
+ });
28
+ }
29
+ }
@@ -0,0 +1,184 @@
1
+ import { inject, injectable } from "@codemation/core";
2
+ import { ApplicationTokens } from "../applicationTokens";
3
+ import type { LoggerFactory } from "../application/logging/Logger";
4
+ import { McpServerCatalog } from "./McpServerCatalog";
5
+ import { DefaultMcpClientFactory } from "./McpClientFactory";
6
+ import type { McpClientFactory } from "./McpClientFactory";
7
+ import { CredentialOAuth2MaterialReader } from "../credentials/CredentialOAuth2MaterialReader";
8
+ import type { MCPClient, McpToolSet } from "./McpConnectionPool.types";
9
+
10
+ /** Mutable internal pool entry (toolsCache may be filled lazily). */
11
+ type MutablePoolEntry = {
12
+ client: MCPClient;
13
+ toolsCache: McpToolSet | null;
14
+ openedAt: Date;
15
+ };
16
+
17
+ @injectable()
18
+ export class McpConnectionPool {
19
+ /** Key: `${credentialInstanceId}:${serverId}` */
20
+ private readonly pool = new Map<string, MutablePoolEntry>();
21
+ /**
22
+ * In-flight open promises — prevents a double-open race when two callers request
23
+ * the same (credentialInstanceId, serverId) pair concurrently.
24
+ */
25
+ private readonly inFlight = new Map<string, Promise<MutablePoolEntry>>();
26
+
27
+ constructor(
28
+ @inject(McpServerCatalog) private readonly catalog: McpServerCatalog,
29
+ @inject(CredentialOAuth2MaterialReader) private readonly oauth2Material: CredentialOAuth2MaterialReader,
30
+ @inject(ApplicationTokens.LoggerFactory) private readonly loggers: LoggerFactory,
31
+ @inject(DefaultMcpClientFactory) private readonly clientFactory: McpClientFactory,
32
+ ) {}
33
+
34
+ /**
35
+ * Returns a live MCP client for the given credential instance + server.
36
+ * Opens a new connection lazily; subsequent calls with the same pair return the cached client.
37
+ * Two concurrent calls for the same pair share a single open operation (single-flight).
38
+ */
39
+ async getClient(credentialInstanceId: string, serverId: string): Promise<MCPClient> {
40
+ const entry = await this.getOrOpenEntry(credentialInstanceId, serverId);
41
+ return entry.client;
42
+ }
43
+
44
+ /**
45
+ * Returns the tools/list result for the given credential instance + server,
46
+ * with toolDescriptionOverrides from the declaration applied.
47
+ * Fetches and caches once per pool entry; subsequent calls return the cached value.
48
+ * Used by Story 10's BM25 indexer.
49
+ */
50
+ async getTools(credentialInstanceId: string, serverId: string): Promise<McpToolSet> {
51
+ const entry = await this.getOrOpenEntry(credentialInstanceId, serverId);
52
+ if (!entry.toolsCache) {
53
+ const raw = await entry.client.tools();
54
+ const decl = this.catalog.get(serverId);
55
+ entry.toolsCache = this.applyOverrides(raw, decl?.toolDescriptionOverrides);
56
+ }
57
+ return entry.toolsCache;
58
+ }
59
+
60
+ /**
61
+ * Closes all pool entries for a credential instance.
62
+ * Call this when the credential is revoked or disconnected.
63
+ * Token refresh does NOT require closing — OAuthFlowExecutor
64
+ * keeps the stored token fresh; the next open will read the current token.
65
+ *
66
+ * Resolves after all matched clients have completed close(), so callers can
67
+ * await this before re-connecting or cleaning up downstream state.
68
+ *
69
+ * TODO(story-credential-lifecycle): Wire this method to the credential lifecycle event.
70
+ * CredentialDisconnectedError (packages/host/src/credentials/refresh/CredentialDisconnectedError.ts)
71
+ * is thrown on dead refresh tokens but is an error, not a broadcast event — there is no
72
+ * event bus for credential lifecycle today. When a credential-disconnected event mechanism
73
+ * is introduced, call closeForCredential(credentialInstanceId) from its handler so that
74
+ * stale MCP pool entries are cleaned up on credential revocation.
75
+ */
76
+ async closeForCredential(credentialInstanceId: string): Promise<void> {
77
+ const logger = this.loggers.create("McpConnectionPool");
78
+ const prefix = `${credentialInstanceId}:`;
79
+ const toClose: Array<[string, MutablePoolEntry]> = [];
80
+ for (const [key, entry] of this.pool.entries()) {
81
+ if (key.startsWith(prefix)) {
82
+ toClose.push([key, entry]);
83
+ this.pool.delete(key);
84
+ logger.info(`McpConnectionPool: closed pool entry on credential revocation (key=${key})`);
85
+ }
86
+ }
87
+ await Promise.allSettled(
88
+ toClose.map(([key, entry]) =>
89
+ entry.client.close().catch((e: unknown) => {
90
+ logger.warn(
91
+ `McpConnectionPool: error closing client on credential revocation (key=${key})`,
92
+ e instanceof Error ? e : undefined,
93
+ );
94
+ }),
95
+ ),
96
+ );
97
+ }
98
+
99
+ /**
100
+ * Closes all pool entries. Called on host shutdown.
101
+ */
102
+ async closeAll(): Promise<void> {
103
+ await Promise.allSettled([...this.pool.values()].map((e) => e.client.close()));
104
+ this.pool.clear();
105
+ this.inFlight.clear();
106
+ }
107
+
108
+ private async getOrOpenEntry(credentialInstanceId: string, serverId: string): Promise<MutablePoolEntry> {
109
+ const key = this.poolKey(credentialInstanceId, serverId);
110
+ const cached = this.pool.get(key);
111
+ if (cached) {
112
+ return cached;
113
+ }
114
+ const existing = this.inFlight.get(key);
115
+ if (existing) {
116
+ return existing;
117
+ }
118
+ const openPromise = this.open(credentialInstanceId, serverId, key).finally(() => {
119
+ this.inFlight.delete(key);
120
+ });
121
+ this.inFlight.set(key, openPromise);
122
+ return openPromise;
123
+ }
124
+
125
+ private async open(credentialInstanceId: string, serverId: string, key: string): Promise<MutablePoolEntry> {
126
+ const decl = this.catalog.get(serverId);
127
+ if (!decl) {
128
+ throw new Error(`McpConnectionPool: MCP server "${serverId}" not found in catalog`);
129
+ }
130
+ // D1: HTTP-only in managed mode. The catalog already blocks stdio at merge time, but we
131
+ // double-check here as a defensive guard in case a declaration bypasses the catalog (e.g.
132
+ // a future in-memory test harness or a dynamically injected server that skips catalog
133
+ // validation). Transport is an attribute of the declaration, not the env var.
134
+ if (decl.transport !== "http") {
135
+ throw new Error(
136
+ `McpConnectionPool: MCP server "${serverId}" uses transport "${decl.transport}" which is not allowed in managed mode. ` +
137
+ `Only "http" transport is supported. For stdio, set CODEMATION_ALLOW_STDIO_MCP=true in a self-hosted environment.`,
138
+ );
139
+ }
140
+
141
+ // Read OAuth material directly. The bearer is baked into the client's headers at open
142
+ // time (per-open, not per-call) — @ai-sdk/mcp v1.0.42 does not support per-request header
143
+ // injection. LIMITATION: a pool entry uses a stale bearer after the access token expires;
144
+ // OAuthFlowExecutor refreshes stored material in the background, but the entry must be
145
+ // closed via closeForCredential and re-opened for the refreshed token to take effect.
146
+ const accessToken = await this.readAccessToken(credentialInstanceId, serverId);
147
+ const headers: Record<string, string> = {
148
+ ...(decl.staticHeaders ?? {}),
149
+ authorization: `Bearer ${accessToken}`,
150
+ };
151
+
152
+ const client = await this.clientFactory.open({ url: decl.url, headers });
153
+ const entry: MutablePoolEntry = { client, toolsCache: null, openedAt: new Date() };
154
+ this.pool.set(key, entry);
155
+ return entry;
156
+ }
157
+
158
+ private poolKey(credentialInstanceId: string, serverId: string): string {
159
+ return `${credentialInstanceId}:${serverId}`;
160
+ }
161
+
162
+ private async readAccessToken(credentialInstanceId: string, serverId: string): Promise<string> {
163
+ const material = await this.oauth2Material.readMaterial(credentialInstanceId);
164
+ if (!material.accessToken) {
165
+ throw new Error(
166
+ `McpConnectionPool: credential instance "${credentialInstanceId}" has no access token — reconnect the credential bound to MCP server "${serverId}"`,
167
+ );
168
+ }
169
+ return material.accessToken;
170
+ }
171
+
172
+ private applyOverrides(tools: McpToolSet, overrides?: Record<string, string>): McpToolSet {
173
+ if (!overrides) {
174
+ return tools;
175
+ }
176
+ const result = { ...tools };
177
+ for (const [name, description] of Object.entries(overrides)) {
178
+ if (result[name]) {
179
+ result[name] = { ...result[name], description };
180
+ }
181
+ }
182
+ return result;
183
+ }
184
+ }
@@ -0,0 +1,12 @@
1
+ import type { MCPClient } from "@ai-sdk/mcp";
2
+
3
+ export type { MCPClient };
4
+
5
+ /** The ToolSet shape returned by MCPClient.tools() with 'automatic' schema resolution. */
6
+ export type McpToolSet = Awaited<ReturnType<MCPClient["tools"]>>;
7
+
8
+ export type McpPoolEntry = Readonly<{
9
+ client: MCPClient;
10
+ toolsCache: McpToolSet | null;
11
+ openedAt: Date;
12
+ }>;