@codemation/host 0.6.0 → 0.8.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.
- package/CHANGELOG.md +483 -0
- package/dist/{ApiPaths-CLTHphYZ.js → ApiPaths-Dv1dcHu_.js} +4 -4
- package/dist/ApiPaths-Dv1dcHu_.js.map +1 -0
- package/dist/{AppConfigFactory-YnveXE9k.d.ts → AppConfigFactory-BT0y0LVC.d.ts} +8490 -5548
- package/dist/{AppConfigFactory-C6q-CSKb.js → AppConfigFactory-Cx4qQvRk.js} +112 -52
- package/dist/AppConfigFactory-Cx4qQvRk.js.map +1 -0
- package/dist/{AppContainerFactory-qaqc-R1D.js → AppContainerFactory-DRTjG7nG.js} +7298 -1732
- package/dist/AppContainerFactory-DRTjG7nG.js.map +1 -0
- package/dist/{CodemationAppContext-DRu1Dpri.d.ts → CodemationAppContext-CGFYVcSb.d.ts} +14 -4
- package/dist/{CodemationAuthoring.types-DZl-sJaM.js → CodemationAuthoring.types-BteaR3Dc.js} +19 -6
- package/dist/CodemationAuthoring.types-BteaR3Dc.js.map +1 -0
- package/dist/{CodemationAuthoring.types-fBRppnmi.d.ts → CodemationAuthoring.types-DiKKogum.d.ts} +30 -5
- package/dist/{CodemationConfigNormalizer-DVko3cVN.d.ts → CodemationConfigNormalizer-48f-T66P.d.ts} +3 -3
- package/dist/{CodemationConsumerConfigLoader-BeAUS144.js → CodemationConsumerConfigLoader-By-6tuGc.js} +81 -10
- package/dist/CodemationConsumerConfigLoader-By-6tuGc.js.map +1 -0
- package/dist/{CodemationConsumerConfigLoader-DJWr86f-.d.ts → CodemationConsumerConfigLoader-_PIYqwVx.d.ts} +18 -2
- package/dist/{CodemationPluginListMerger-B-W5Fa_X.js → CodemationPluginListMerger-D1B1IEbt.js} +1 -1
- package/dist/{CodemationPluginListMerger-B-W5Fa_X.js.map → CodemationPluginListMerger-D1B1IEbt.js.map} +1 -1
- package/dist/{CodemationPluginListMerger-DGc-jfa2.d.ts → CodemationPluginListMerger-DP7djJ9S.d.ts} +151 -19
- package/dist/CodemationTsyringeTypeInfoRegistrar-Bj6FJYFz.js +97 -0
- package/dist/CodemationTsyringeTypeInfoRegistrar-Bj6FJYFz.js.map +1 -0
- package/dist/{CodemationWhitelabelConfig-CWbcyQqn.d.ts → CodemationWhitelabelConfig-Ca2mCUeC.d.ts} +2 -2
- package/dist/{CollectionContracts.types-DdpHft0i.d.ts → CollectionContracts.types-DDyFYT_D.d.ts} +1 -1
- package/dist/{CredentialContractsRegistry-DrMIDSw8.d.ts → CredentialContractsRegistry-Bq2bq28t.d.ts} +2 -2
- package/dist/{CredentialServices-UfvHB-rN.d.ts → CredentialServices-BLloBztI.d.ts} +65 -20
- package/dist/{CredentialServices-CgxwguAv.js → CredentialServices-Dk8yypeL.js} +310 -51
- package/dist/CredentialServices-Dk8yypeL.js.map +1 -0
- package/dist/InternalHonoApiRouteRegistrar-c7t3KnV_.d.ts +17 -0
- package/dist/InternalPingRegistrar-DY3kSfxP.js +221 -0
- package/dist/InternalPingRegistrar-DY3kSfxP.js.map +1 -0
- package/dist/{ItemsInputNormalizer-C-KHg9Mo.d.ts → ItemsInputNormalizer-_RwIfRIQ.d.ts} +89 -25
- package/dist/{LogLevelPolicyFactory-CampWO0l.d.ts → LogLevelPolicyFactory-ewCHLDLn.d.ts} +2 -2
- package/dist/{PublicFrontendBootstrap-DzBgwOnG.d.ts → PublicFrontendBootstrap-Cev3qK46.d.ts} +9 -2
- package/dist/PublicFrontendBootstrapFactory-Dv04tJ-6.d.ts +82 -0
- package/dist/{PublicFrontendBootstrapJsonCodec-Cl_DLRh0.d.ts → PublicFrontendBootstrapJsonCodec-CXG9Dxft.d.ts} +3 -3
- package/dist/{PublicFrontendBootstrapJsonCodec-DzqvA0uo.js → PublicFrontendBootstrapJsonCodec-CegIF_ne.js} +7 -2
- package/dist/PublicFrontendBootstrapJsonCodec-CegIF_ne.js.map +1 -0
- package/dist/ServerLoggerFactory-Ckk52S3w.js +223 -0
- package/dist/ServerLoggerFactory-Ckk52S3w.js.map +1 -0
- package/dist/{TelemetryContracts-DbaNomrH.d.ts → TelemetryContracts-BtDx84Cp.d.ts} +13 -4
- package/dist/{WorkflowPolicyUiPresentationFactory-DQEY-h_S.d.ts → WorkflowPolicyUiPresentationFactory-6MyjCvBO.d.ts} +2 -2
- package/dist/{WorkflowPolicyUiPresentationFactory-DhPqQ9aB.js → WorkflowPolicyUiPresentationFactory-Bb-ae_Zh.js} +1 -1
- package/dist/{WorkflowPolicyUiPresentationFactory-DhPqQ9aB.js.map → WorkflowPolicyUiPresentationFactory-Bb-ae_Zh.js.map} +1 -1
- package/dist/{WorkflowViewContracts-CzK2KFuz.d.ts → WorkflowViewContracts-B7aFQcIw.d.ts} +10 -1
- package/dist/authoring.d.ts +5 -5
- package/dist/authoring.js +1 -1
- package/dist/client.d.ts +4 -4
- package/dist/client.js +2 -2
- package/dist/consumer.d.ts +6 -6
- package/dist/consumer.js +2 -2
- package/dist/credentials.d.ts +6 -6
- package/dist/credentials.js +1 -1
- package/dist/devServerSidecar.d.ts +2 -2
- package/dist/devServerSidecar.js +1 -94
- package/dist/devServerSidecar.js.map +1 -1
- package/dist/dto.d.ts +6 -6
- package/dist/{index-BbBk26m0.d.ts → index-DilAYwnH.d.ts} +49 -3
- package/dist/index.d.ts +141 -21
- package/dist/index.js +109 -14
- package/dist/index.js.map +1 -0
- package/dist/mapping.d.ts +2 -2
- package/dist/mapping.js +1 -1
- package/dist/nextServer.d.ts +42 -113
- package/dist/nextServer.js +9 -7
- package/dist/pairing.d.ts +93 -0
- package/dist/pairing.js +5 -0
- package/dist/pairing.types-snfZ_OzB.d.ts +19 -0
- package/dist/persistenceServer-B71RGvSj.d.ts +30 -0
- package/dist/{persistenceServer-CmsIKnO9.js → persistenceServer-C-hH4z6l.js} +2 -2
- package/dist/{persistenceServer-CmsIKnO9.js.map → persistenceServer-C-hH4z6l.js.map} +1 -1
- package/dist/persistenceServer.d.ts +8 -8
- package/dist/persistenceServer.js +3 -3
- package/dist/{server-MUNGsBYK.d.ts → server-09PKasWR.d.ts} +21 -6
- package/dist/{server-CJFfY67o.js → server-vtRCPgRJ.js} +7 -6
- package/dist/{server-CJFfY67o.js.map → server-vtRCPgRJ.js.map} +1 -1
- package/dist/server.d.ts +14 -14
- package/dist/server.js +13 -11
- package/package.json +47 -58
- package/prisma/migrations/20260519000000_workflow_audit_log/migration.sql +23 -0
- package/prisma/migrations/20260519100000_storage_growth_fixes/migration.sql +61 -0
- package/prisma/migrations.sqlite/20260519000000_workflow_audit_log/migration.sql +21 -0
- package/prisma/migrations.sqlite/20260519100000_storage_growth_fixes/migration.sql +29 -0
- package/prisma/schema.postgresql.prisma +55 -17
- package/prisma/schema.sqlite.prisma +55 -17
- package/prisma-generated/prisma-postgresql-client/edge.js +33 -5
- package/prisma-generated/prisma-postgresql-client/index-browser.js +29 -1
- package/prisma-generated/prisma-postgresql-client/index.d.ts +8933 -5716
- package/prisma-generated/prisma-postgresql-client/index.js +33 -5
- package/prisma-generated/prisma-postgresql-client/package.json +1 -1
- package/prisma-generated/prisma-postgresql-client/schema.prisma +38 -0
- package/prisma-generated/prisma-sqlite-client/edge.js +33 -5
- package/prisma-generated/prisma-sqlite-client/index-browser.js +29 -1
- package/prisma-generated/prisma-sqlite-client/index.d.ts +8925 -5713
- package/prisma-generated/prisma-sqlite-client/index.js +33 -5
- package/prisma-generated/prisma-sqlite-client/package.json +1 -1
- package/prisma-generated/prisma-sqlite-client/schema.prisma +38 -0
- package/scripts/check-collections.mjs +18 -0
- package/scripts/generate-prisma-clients.mjs +20 -11
- package/src/application/WorkflowAuditLogPruneScheduler.ts +96 -0
- package/src/application/auth/AuthenticatedPrincipal.ts +4 -0
- package/src/application/commands/StartWorkflowRunCommandHandler.ts +4 -0
- package/src/application/contracts/WorkflowViewContracts.ts +6 -0
- package/src/application/contracts/WorkflowWebsocketMessage.ts +3 -1
- package/src/application/mapping/WorkflowDefinitionMapper.ts +40 -1
- package/src/application/runs/WorkflowRunRetentionPruneScheduler.ts +7 -1
- package/src/application/telemetry/OtelExecutionTelemetry.types.ts +5 -0
- package/src/application/telemetry/OtelExecutionTelemetryFactory.ts +4 -0
- package/src/application/telemetry/StoredTelemetrySpanScope.ts +6 -2
- package/src/application/telemetry/TelemetryRetentionTimestampFactory.ts +27 -17
- package/src/application/telemetry/TelemetrySpanPublisher.ts +11 -0
- package/src/application/websocket/TelemetrySpanWebsocketRelay.ts +31 -0
- package/src/applicationTokens.ts +20 -1
- package/src/audit/IAuditEmitter.ts +32 -0
- package/src/audit/PrismaWorkflowAuditLogRepository.ts +34 -0
- package/src/audit/WorkflowAuditLogWriter.ts +125 -0
- package/src/auth/managed/ManagedAuthConfig.ts +29 -0
- package/src/auth/managed/ManagedAuthMiddleware.ts +52 -0
- package/src/auth/managed/ManagedCorsMiddleware.ts +43 -0
- package/src/auth/managed/ManagedModeBootGuard.ts +27 -0
- package/src/auth/managed/index.ts +5 -0
- package/src/bootstrap/AppContainerFactory.ts +295 -29
- package/src/bootstrap/AppContainerLifecycle.ts +31 -0
- package/src/bootstrap/perf/BootTimer.ts +168 -0
- package/src/bootstrap/runtime/AppConfigFactory.ts +21 -65
- package/src/bootstrap/runtime/FrontendRuntime.ts +4 -1
- package/src/bootstrap/runtime/HeadlessApiRuntime.ts +47 -0
- package/src/bootstrap/runtime/WorkerRuntime.ts +2 -1
- package/src/credentials/BrokerClient.ts +49 -0
- package/src/credentials/BrokerRefreshError.ts +12 -0
- package/src/credentials/BrokerRefreshInvalidGrantError.ts +13 -0
- package/src/credentials/ControlPlaneCatalogFetcher.ts +261 -0
- package/src/credentials/CredentialOAuth2MaterialReader.ts +136 -0
- package/src/credentials/InternalCredentialsListRegistrar.ts +48 -0
- package/src/credentials/InternalCredentialsPushRegistrar.ts +125 -0
- package/src/credentials/LocalOAuthFlowExecutor.ts +316 -0
- package/src/credentials/ManagedOAuthFlowExecutor.ts +94 -0
- package/src/credentials/ManagedOAuthRefreshInvalidGrantError.ts +13 -0
- package/src/credentials/catalogTypes.ts +4 -0
- package/src/credentials/refresh/CredentialDisconnectedError.ts +11 -0
- package/src/domain/credentials/CredentialBindingService.ts +54 -2
- package/src/domain/credentials/CredentialKeyRotatedError.ts +22 -0
- package/src/domain/credentials/CredentialSecretCipher.ts +68 -6
- package/src/domain/credentials/CredentialTypeRegistryImpl.ts +117 -10
- package/src/domain/credentials/OAuth2RedirectUriResolver.ts +79 -0
- package/src/domain/credentials/WorkflowCredentialNodeResolver.ts +14 -5
- package/src/domain/telemetry/TelemetryContracts.ts +7 -1
- package/src/domain/workflows/WorkflowActivationPreflight.ts +24 -1
- package/src/domain/workflows/WorkflowActivationPreflightRules.ts +40 -1
- package/src/index.ts +9 -0
- package/src/infrastructure/binary/LocalFilesystemBinaryStorageRegistry.ts +29 -1
- package/src/infrastructure/binary/S3BinaryStorage.ts +169 -0
- package/src/infrastructure/binary/S3BinaryStorageConfig.ts +17 -0
- package/src/infrastructure/config/CodemationPluginRegistrar.ts +3 -1
- package/src/infrastructure/persistence/CodemationDatabaseUrlParser.ts +41 -0
- package/src/infrastructure/persistence/InMemoryTelemetryArtifactStore.ts +8 -3
- package/src/infrastructure/persistence/PrismaMigrationDeployer.ts +21 -13
- package/src/infrastructure/persistence/PrismaTelemetryArtifactStore.ts +43 -8
- package/src/infrastructure/persistence/PrismaWorkflowRunRepository.ts +26 -3
- package/src/infrastructure/persistence/PrismaWorkflowSnapshotRepository.ts +48 -0
- package/src/mcp/AgentMcpIntegrationImpl.ts +344 -0
- package/src/mcp/McpClientFactory.ts +29 -0
- package/src/mcp/McpConnectionPool.ts +184 -0
- package/src/mcp/McpConnectionPool.types.ts +12 -0
- package/src/mcp/McpServerCatalog.ts +104 -0
- package/src/mcp/index.ts +5 -0
- package/src/pairing/HmacRequestSigner.ts +32 -0
- package/src/pairing/IncomingHmacVerifier.ts +82 -0
- package/src/pairing/InternalHmacAuthMiddleware.ts +33 -0
- package/src/pairing/InternalPingRegistrar.ts +25 -0
- package/src/pairing/PairedFetch.ts +33 -0
- package/src/pairing/PairingConfigFactory.ts +35 -0
- package/src/pairing/PairingConfigToken.ts +6 -0
- package/src/pairing/index.ts +14 -0
- package/src/pairing/pairing.types.ts +18 -0
- package/src/pairing.ts +17 -0
- package/src/persistenceServer.ts +1 -0
- package/src/presentation/config/AppConfig.ts +7 -1
- package/src/presentation/config/CodemationAuthConfig.ts +1 -1
- package/src/presentation/config/CodemationAuthoring.types.ts +60 -5
- package/src/presentation/config/CodemationConfig.ts +9 -0
- package/src/presentation/config/CodemationConfigNormalizer.ts +39 -1
- package/src/presentation/config/CodemationPlugin.ts +2 -1
- package/src/presentation/frontend/CodemationFrontendAuthSnapshot.ts +5 -0
- package/src/presentation/frontend/CodemationFrontendAuthSnapshotFactory.ts +7 -1
- package/src/presentation/frontend/PublicFrontendBootstrap.ts +2 -0
- package/src/presentation/frontend/PublicFrontendBootstrapFactory.ts +5 -1
- package/src/presentation/frontend/PublicFrontendBootstrapJsonCodec.ts +4 -1
- package/src/presentation/http/ApiPaths.ts +4 -4
- package/src/presentation/http/HeadlessHttpServerFactory.ts +56 -0
- package/src/presentation/http/ServerHttpErrorResponseFactory.ts +39 -2
- package/src/presentation/http/hono/CodemationHonoApiAppFactory.ts +33 -8
- package/src/presentation/http/hono/InternalHonoApiRouteRegistrar.ts +12 -0
- package/src/presentation/http/hono/registrars/ManagedMeHonoApiRouteRegistrar.ts +35 -0
- package/src/presentation/http/hono/registrars/OAuth2HonoApiRouteRegistrar.ts +2 -2
- package/src/presentation/http/routeHandlers/CredentialHttpRouteHandler.ts +28 -0
- package/src/presentation/http/routeHandlers/OAuth2HttpRouteHandlerFactory.ts +98 -41
- package/src/presentation/server/CodemationConsumerConfigLoader.ts +59 -7
- package/src/presentation/server/CodemationPluginDiscovery.ts +5 -0
- package/src/presentation/server/WorkflowDefinitionExportsResolver.ts +18 -0
- package/src/presentation/server/WorkflowModulePathFinder.ts +12 -1
- package/src/presentation/websocket/ManagedWebsocketAuthenticator.ts +50 -0
- package/src/presentation/websocket/WebsocketAuthenticator.types.ts +12 -0
- package/src/presentation/websocket/WorkflowWebsocketServer.ts +24 -3
- package/src/presentation/websocket/WorkflowWebsocketServerFactory.ts +16 -0
- package/src/process/ExecaProcessRunner.ts +41 -0
- package/src/process/ProcessRunner.types.ts +39 -0
- package/src/server.ts +2 -0
- package/src/workflows/InternalWorkflowActivationRegistrar.ts +42 -0
- package/src/workflows/InternalWorkflowDetailRegistrar.ts +33 -0
- package/src/workflows/InternalWorkflowTestRunRegistrar.ts +91 -0
- package/src/workflows/InternalWorkflowsListRegistrar.ts +28 -0
- package/src/workflows/discovery/WorkflowDirectoryDiscoverer.ts +79 -0
- package/tsconfig.json +2 -0
- package/vitest.shared.ts +5 -0
- package/dist/ApiPaths-CLTHphYZ.js.map +0 -1
- package/dist/AppConfigFactory-C6q-CSKb.js.map +0 -1
- package/dist/AppContainerFactory-qaqc-R1D.js.map +0 -1
- package/dist/CodemationAuthoring.types-DZl-sJaM.js.map +0 -1
- package/dist/CodemationConsumerConfigLoader-BeAUS144.js.map +0 -1
- package/dist/CredentialServices-CgxwguAv.js.map +0 -1
- package/dist/PublicFrontendBootstrapFactory-Cb2pLmDd.d.ts +0 -45
- package/dist/PublicFrontendBootstrapJsonCodec-DzqvA0uo.js.map +0 -1
- package/dist/ServerLoggerFactory-BKSIh9Xv.js +0 -98
- package/dist/ServerLoggerFactory-BKSIh9Xv.js.map +0 -1
- package/dist/persistenceServer-vtJAGDat.d.ts +0 -9
- package/src/domain/credentials/OAuth2ConnectServiceFactory.ts +0 -411
|
@@ -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
|
+
}>;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { inject, injectable } from "@codemation/core";
|
|
2
|
+
import type { McpServerDeclaration } from "@codemation/core";
|
|
3
|
+
import { ApplicationTokens } from "../applicationTokens";
|
|
4
|
+
import type { LoggerFactory } from "../application/logging/Logger";
|
|
5
|
+
import type { AppConfig } from "../presentation/config/AppConfig";
|
|
6
|
+
|
|
7
|
+
export type McpServerDeclarationSource = "plugin" | "config" | "controlPlane";
|
|
8
|
+
|
|
9
|
+
const SOURCE_PRIORITY: Record<McpServerDeclarationSource, number> = {
|
|
10
|
+
plugin: 0,
|
|
11
|
+
config: 1,
|
|
12
|
+
controlPlane: 2,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const ID_PATTERN = /^[a-z0-9-]+$/;
|
|
16
|
+
|
|
17
|
+
type CatalogEntry = Readonly<{
|
|
18
|
+
decl: McpServerDeclaration;
|
|
19
|
+
source: McpServerDeclarationSource;
|
|
20
|
+
}>;
|
|
21
|
+
|
|
22
|
+
@injectable()
|
|
23
|
+
export class McpServerCatalog {
|
|
24
|
+
private readonly entries = new Map<string, CatalogEntry>();
|
|
25
|
+
private readonly bySource = new Map<McpServerDeclarationSource, Set<string>>();
|
|
26
|
+
private readonly env: NodeJS.ProcessEnv;
|
|
27
|
+
|
|
28
|
+
constructor(
|
|
29
|
+
@inject(ApplicationTokens.LoggerFactory) private readonly loggers: LoggerFactory,
|
|
30
|
+
@inject(ApplicationTokens.AppConfig) appConfig: AppConfig,
|
|
31
|
+
) {
|
|
32
|
+
this.env = appConfig.env;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
merge(source: McpServerDeclarationSource, declarations: ReadonlyArray<McpServerDeclaration>): void {
|
|
36
|
+
const logger = this.loggers.create("McpServerCatalog");
|
|
37
|
+
for (const decl of declarations) {
|
|
38
|
+
if (!this.validate(decl, source, logger)) {
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
const existing = this.entries.get(decl.id);
|
|
42
|
+
if (existing) {
|
|
43
|
+
if (SOURCE_PRIORITY[source] <= SOURCE_PRIORITY[existing.source]) {
|
|
44
|
+
logger.warn(
|
|
45
|
+
`McpServerCatalog: id collision — lower-priority source "${source}" ignored for id "${decl.id}" (current source: "${existing.source}")`,
|
|
46
|
+
);
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
logger.warn(
|
|
50
|
+
`McpServerCatalog: id "${decl.id}" shadowed — "${existing.source}" overridden by higher-priority source "${source}"`,
|
|
51
|
+
);
|
|
52
|
+
this.bySource.get(existing.source)?.delete(decl.id);
|
|
53
|
+
}
|
|
54
|
+
this.entries.set(decl.id, { decl, source });
|
|
55
|
+
if (!this.bySource.has(source)) {
|
|
56
|
+
this.bySource.set(source, new Set());
|
|
57
|
+
}
|
|
58
|
+
this.bySource.get(source)!.add(decl.id);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
get(id: string): McpServerDeclaration | undefined {
|
|
63
|
+
return this.entries.get(id)?.decl;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
getAll(): readonly McpServerDeclaration[] {
|
|
67
|
+
return [...this.entries.values()].map((entry) => entry.decl);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
clear(source: McpServerDeclarationSource): void {
|
|
71
|
+
const ids = this.bySource.get(source);
|
|
72
|
+
if (!ids) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
for (const id of ids) {
|
|
76
|
+
this.entries.delete(id);
|
|
77
|
+
}
|
|
78
|
+
this.bySource.delete(source);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private validate(
|
|
82
|
+
decl: McpServerDeclaration,
|
|
83
|
+
source: McpServerDeclarationSource,
|
|
84
|
+
logger: ReturnType<LoggerFactory["create"]>,
|
|
85
|
+
): boolean {
|
|
86
|
+
if (!ID_PATTERN.test(decl.id)) {
|
|
87
|
+
logger.warn(
|
|
88
|
+
`McpServerCatalog: declaration from "${source}" has invalid id "${decl.id}" (must match /^[a-z0-9-]+$/) — skipped`,
|
|
89
|
+
);
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if ((decl.transport as string) === "stdio") {
|
|
94
|
+
if (this.env.CODEMATION_ALLOW_STDIO_MCP !== "true") {
|
|
95
|
+
logger.warn(
|
|
96
|
+
`McpServerCatalog: declaration "${decl.id}" from "${source}" uses stdio transport which is disabled (set CODEMATION_ALLOW_STDIO_MCP=true to allow) — skipped`,
|
|
97
|
+
);
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
}
|
package/src/mcp/index.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { McpServerCatalog, type McpServerDeclarationSource } from "./McpServerCatalog";
|
|
2
|
+
export { McpConnectionPool } from "./McpConnectionPool";
|
|
3
|
+
export type { McpPoolEntry, McpToolSet, MCPClient } from "./McpConnectionPool.types";
|
|
4
|
+
export { DefaultMcpClientFactory, type McpClientFactory, type McpClientOpenArgs } from "./McpClientFactory";
|
|
5
|
+
export { AgentMcpIntegrationImpl } from "./AgentMcpIntegrationImpl";
|