@jini-ai/mcp 0.3.3 → 0.4.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 +151 -0
- package/README.md +97 -22
- package/dist/agent-install/install.d.ts +20 -4
- package/dist/agent-install/install.js +11 -11
- package/dist/bin/serve.d.ts +25 -13
- package/dist/bin/serve.js +42 -21
- package/dist/client/client.d.ts +28 -8
- package/dist/client/client.js +25 -21
- package/dist/core/config.d.ts +57 -18
- package/dist/core/config.js +76 -48
- package/dist/core/index.d.ts +1 -2
- package/dist/core/index.js +1 -2
- package/dist/core/install-info.d.ts +4 -1
- package/dist/core/install-info.js +5 -5
- package/dist/core/tokens.d.ts +39 -13
- package/dist/core/tokens.js +54 -24
- package/dist/federation/adapter.http.d.ts +67 -0
- package/dist/federation/adapter.http.js +414 -0
- package/dist/federation/approvals/call-confirmation.d.ts +23 -0
- package/dist/federation/approvals/call-confirmation.js +124 -0
- package/dist/federation/approvals/index.d.ts +4 -0
- package/dist/federation/approvals/index.js +4 -0
- package/dist/federation/approvals/ports.d.ts +107 -0
- package/dist/federation/approvals/ports.js +2 -0
- package/dist/federation/approvals/revocation.d.ts +61 -0
- package/dist/federation/approvals/revocation.js +52 -0
- package/dist/federation/bootstrap.d.ts +100 -0
- package/dist/federation/bootstrap.js +182 -0
- package/dist/federation/config.d.ts +68 -0
- package/dist/federation/config.js +40 -0
- package/dist/federation/index.d.ts +14 -0
- package/dist/federation/index.js +14 -0
- package/dist/federation/mcp-protocol.d.ts +115 -0
- package/dist/federation/mcp-protocol.js +151 -0
- package/dist/federation/messages.d.ts +54 -0
- package/dist/federation/messages.js +35 -0
- package/dist/federation/ports.d.ts +287 -0
- package/dist/federation/ports.js +5 -0
- package/dist/federation/presets.d.ts +66 -0
- package/dist/federation/presets.js +30 -0
- package/dist/federation/refusal-notice.d.ts +198 -0
- package/dist/federation/refusal-notice.js +193 -0
- package/dist/federation/registrations.d.ts +171 -0
- package/dist/federation/registrations.js +181 -0
- package/dist/federation/reload.d.ts +112 -0
- package/dist/federation/reload.js +110 -0
- package/dist/federation/stdio/adapter.stdio.d.ts +157 -0
- package/dist/federation/stdio/adapter.stdio.js +526 -0
- package/dist/federation/stdio/default-connect.d.ts +29 -0
- package/dist/federation/stdio/default-connect.js +92 -0
- package/dist/federation/stdio/index.d.ts +4 -0
- package/dist/federation/stdio/index.js +4 -0
- package/dist/federation/stdio/stdio-launch-resolver.d.ts +139 -0
- package/dist/federation/stdio/stdio-launch-resolver.js +227 -0
- package/dist/federation/testing/adapter.memory.d.ts +177 -0
- package/dist/federation/testing/adapter.memory.js +189 -0
- package/dist/federation/testing/index.d.ts +3 -0
- package/dist/federation/testing/index.js +3 -0
- package/dist/federation/testing/tool-approvals.memory.d.ts +27 -0
- package/dist/federation/testing/tool-approvals.memory.js +38 -0
- package/dist/federation/tool-approvals.d.ts +97 -0
- package/dist/federation/tool-approvals.js +42 -0
- package/dist/federation/trust.d.ts +471 -0
- package/dist/federation/trust.js +707 -0
- package/dist/index.d.ts +175 -2
- package/dist/index.js +172 -2
- package/dist/server/daemon-client.d.ts +16 -4
- package/dist/server/daemon-client.js +41 -19
- package/dist/server/ports.d.ts +9 -0
- package/dist/server/ports.js +2 -0
- package/dist/server/resource-protocol.d.ts +14 -4
- package/dist/server/resource-protocol.js +6 -6
- package/dist/server/resources/active-resource.js +3 -3
- package/dist/server/sdk-transport.d.ts +11 -0
- package/dist/server/sdk-transport.js +31 -0
- package/dist/server/tool-protocol.d.ts +31 -9
- package/dist/server/tool-protocol.js +20 -18
- package/dist/server/tool-server.d.ts +60 -43
- package/dist/server/tool-server.js +51 -27
- package/dist/server/tools/component-catalog-tools.js +7 -7
- package/dist/server/tools/delegated-tool.d.ts +7 -4
- package/dist/server/tools/delegated-tool.js +14 -15
- package/dist/server/tools/run-tools.js +15 -15
- package/dist/server/tools/tool-catalog-tools.js +7 -7
- package/dist/tools/ask-choice/index.d.ts +123 -0
- package/dist/tools/ask-choice/index.js +98 -0
- package/dist/tools/ask-choice/pending.d.ts +11 -0
- package/dist/tools/ask-choice/pending.js +34 -0
- package/dist/tools/ask-choice/question.d.ts +16 -0
- package/dist/tools/ask-choice/question.js +60 -0
- package/dist/tools/ask-choice/schema.d.ts +91 -0
- package/dist/tools/ask-choice/schema.js +27 -0
- package/dist/tools/ask-choice/types.d.ts +106 -0
- package/dist/tools/ask-choice/types.js +2 -0
- package/package.json +43 -5
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import type { Clock } from "@jini-ai/core/primitives";
|
|
2
|
+
import type { FederationMessages } from "../messages.js";
|
|
3
|
+
import type { FederatedCallConfirmationRequest } from "../ports.js";
|
|
4
|
+
import type { ConversationToolApprovalStore, ExternalMcpToolApprovalRepoPort } from "../tool-approvals.js";
|
|
5
|
+
/** Minimal execution identity; the application may pass its richer context unchanged. */
|
|
6
|
+
export interface FederatedConfirmationContext {
|
|
7
|
+
readonly principal: {
|
|
8
|
+
readonly id: string;
|
|
9
|
+
};
|
|
10
|
+
readonly run: {
|
|
11
|
+
readonly id: string;
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
/** Presentation data only. Rendering and exchange lifecycle belong to host adapters. */
|
|
15
|
+
export interface FederatedConfirmationSpec {
|
|
16
|
+
readonly toolId: string;
|
|
17
|
+
readonly errorCode: string;
|
|
18
|
+
readonly title: string;
|
|
19
|
+
readonly description: string;
|
|
20
|
+
readonly details: readonly {
|
|
21
|
+
readonly label: string;
|
|
22
|
+
readonly value: string;
|
|
23
|
+
readonly format?: "code";
|
|
24
|
+
}[];
|
|
25
|
+
readonly warning: string;
|
|
26
|
+
readonly danger: boolean;
|
|
27
|
+
readonly confirmLabel: string;
|
|
28
|
+
readonly alternatives?: readonly {
|
|
29
|
+
readonly id: string;
|
|
30
|
+
readonly label: string;
|
|
31
|
+
readonly choice: "chat" | "always";
|
|
32
|
+
}[];
|
|
33
|
+
}
|
|
34
|
+
export interface FederatedCardOffers {
|
|
35
|
+
readonly offerChat: boolean;
|
|
36
|
+
readonly offerAlways: boolean;
|
|
37
|
+
}
|
|
38
|
+
export type FederatedHumanConfirmOutcome = {
|
|
39
|
+
readonly confirmed: true;
|
|
40
|
+
readonly choice?: string;
|
|
41
|
+
} | {
|
|
42
|
+
readonly confirmed: false;
|
|
43
|
+
readonly result: Readonly<Record<string, unknown>>;
|
|
44
|
+
};
|
|
45
|
+
/** The host binds its exchange store and maps cancelled/expired/abandoned answers to results.
|
|
46
|
+
* Adapters must bind the answer to this principal, tool and execution, and accept it only once.
|
|
47
|
+
*/
|
|
48
|
+
export interface FederatedHumanConfirmPort<Context extends FederatedConfirmationContext, Exchanges> {
|
|
49
|
+
ask(required: {
|
|
50
|
+
context: Context;
|
|
51
|
+
surfaceExchanges: Exchanges;
|
|
52
|
+
spec: FederatedConfirmationSpec;
|
|
53
|
+
}): Promise<FederatedHumanConfirmOutcome>;
|
|
54
|
+
}
|
|
55
|
+
/** Optional remembered scopes; a missing store or conversation removes that choice. */
|
|
56
|
+
export interface FederatedApprovalDeps {
|
|
57
|
+
readonly mayAlwaysAllow: (required: {
|
|
58
|
+
principalId: string;
|
|
59
|
+
}, optional?: {
|
|
60
|
+
scope?: string;
|
|
61
|
+
}) => Promise<boolean>;
|
|
62
|
+
readonly clock: Clock;
|
|
63
|
+
readonly always?: ExternalMcpToolApprovalRepoPort;
|
|
64
|
+
readonly chat?: ConversationToolApprovalStore;
|
|
65
|
+
readonly conversationIdForRun?: (required: {
|
|
66
|
+
runId: string;
|
|
67
|
+
}) => string | undefined;
|
|
68
|
+
}
|
|
69
|
+
/** Host can enqueue durable webhooks; no transport or webhook package is imported here. */
|
|
70
|
+
export interface FederatedApprovalWebhookPort {
|
|
71
|
+
approvalRemembered(required: {
|
|
72
|
+
principalId: string;
|
|
73
|
+
connectionId: string;
|
|
74
|
+
toolName: string;
|
|
75
|
+
fingerprint: string;
|
|
76
|
+
scope: "chat" | "always";
|
|
77
|
+
grantedAt: string;
|
|
78
|
+
}, optional?: {
|
|
79
|
+
scope?: string;
|
|
80
|
+
}): Promise<void>;
|
|
81
|
+
}
|
|
82
|
+
export interface FederatedApprovalDiagnostic {
|
|
83
|
+
readonly toolId: string;
|
|
84
|
+
readonly choice: string;
|
|
85
|
+
readonly stage: "store" | "webhook";
|
|
86
|
+
readonly error: unknown;
|
|
87
|
+
}
|
|
88
|
+
export interface FederatedCallConfirmerRequired<Context extends FederatedConfirmationContext, Exchanges> {
|
|
89
|
+
readonly messages: FederationMessages;
|
|
90
|
+
readonly fingerprintDomain: string;
|
|
91
|
+
readonly errorCode: string;
|
|
92
|
+
readonly surfaceExchanges: Exchanges;
|
|
93
|
+
readonly humanConfirm: FederatedHumanConfirmPort<Context, Exchanges>;
|
|
94
|
+
}
|
|
95
|
+
export interface FederatedCallConfirmerOptions {
|
|
96
|
+
/** Opaque host storage partition; never interpreted by federation. */
|
|
97
|
+
readonly scope?: string;
|
|
98
|
+
readonly approvals?: FederatedApprovalDeps;
|
|
99
|
+
readonly webhooks?: FederatedApprovalWebhookPort;
|
|
100
|
+
readonly onPersistenceError?: (required: FederatedApprovalDiagnostic) => void;
|
|
101
|
+
}
|
|
102
|
+
/** Used by adapters without depending on a tool runtime's callback signature. */
|
|
103
|
+
export interface FederatedConfirmCallInput<Context extends FederatedConfirmationContext> {
|
|
104
|
+
readonly context: Context;
|
|
105
|
+
readonly request: FederatedCallConfirmationRequest;
|
|
106
|
+
}
|
|
107
|
+
//# sourceMappingURL=ports.d.ts.map
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { FederatedCallTarget } from "../ports.js";
|
|
2
|
+
export type ExternalMcpRevocationReason = "removed" | "turned-off" | "disconnected" | "changed" | "tool-not-allowed" | "write-not-allowed" | "unverifiable";
|
|
3
|
+
/** Host projection of the current row; revision derivation and credential status stay in adapters. */
|
|
4
|
+
export interface ConnectionRosterSnapshot {
|
|
5
|
+
readonly label: string | null;
|
|
6
|
+
readonly enabled: boolean;
|
|
7
|
+
readonly admissionRevision: string;
|
|
8
|
+
readonly disconnected: boolean;
|
|
9
|
+
readonly grants: {
|
|
10
|
+
readonly allowedToolNames: readonly string[];
|
|
11
|
+
readonly writeAllowedToolNames: readonly string[];
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
/** A primary-key read per call. The host implements this over its ExternalMcpServerRepoPort. */
|
|
15
|
+
export interface ConnectionRosterReader {
|
|
16
|
+
findByServerId(required: {
|
|
17
|
+
serverId: string;
|
|
18
|
+
}, optional?: {
|
|
19
|
+
scope?: string;
|
|
20
|
+
}): Promise<ConnectionRosterSnapshot | null>;
|
|
21
|
+
}
|
|
22
|
+
export interface FederatedConnectionRefusal {
|
|
23
|
+
readonly serverId: string;
|
|
24
|
+
readonly label: string | null;
|
|
25
|
+
readonly remoteName: string;
|
|
26
|
+
readonly reason: ExternalMcpRevocationReason;
|
|
27
|
+
}
|
|
28
|
+
/** Host preserves its error subclass, validation classification and exact model-facing wording. */
|
|
29
|
+
export interface FederatedRevocationErrorFactoryPort {
|
|
30
|
+
create(required: FederatedConnectionRefusal): Error;
|
|
31
|
+
}
|
|
32
|
+
export interface FederatedRevocationWebhookPort {
|
|
33
|
+
connectionRefused(required: FederatedConnectionRefusal, optional?: {
|
|
34
|
+
scope?: string;
|
|
35
|
+
}): Promise<void>;
|
|
36
|
+
}
|
|
37
|
+
export interface FederatedRevocationDiagnostic {
|
|
38
|
+
readonly connectionId: string;
|
|
39
|
+
readonly stage: "store" | "webhook";
|
|
40
|
+
readonly error: unknown;
|
|
41
|
+
}
|
|
42
|
+
/** Pure current-row check. Ordering matches admission semantics: identity before mutable grants. */
|
|
43
|
+
export declare function rosterRefusalFor({ record, call }: {
|
|
44
|
+
record: ConnectionRosterSnapshot | null;
|
|
45
|
+
call: FederatedCallTarget;
|
|
46
|
+
}): ExternalMcpRevocationReason | null;
|
|
47
|
+
/** Revocation half of a connection gate; credential refresh/reauth checks are composed by the host.
|
|
48
|
+
* Presets have no roster row. Reads and notification failures never disclose driver errors.
|
|
49
|
+
*/
|
|
50
|
+
export declare function createFederatedConnectionRevocationGate(required: {
|
|
51
|
+
roster: ConnectionRosterReader;
|
|
52
|
+
errorFactory: FederatedRevocationErrorFactoryPort;
|
|
53
|
+
}, optional?: {
|
|
54
|
+
scope?: string;
|
|
55
|
+
webhooks?: FederatedRevocationWebhookPort;
|
|
56
|
+
onDiagnostic?: (required: FederatedRevocationDiagnostic) => void;
|
|
57
|
+
}): (required: {
|
|
58
|
+
connectionId: string;
|
|
59
|
+
call: FederatedCallTarget;
|
|
60
|
+
}) => Promise<void>;
|
|
61
|
+
//# sourceMappingURL=revocation.d.ts.map
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { refusalForAdmittedToolUnderCurrentGrants } from "../trust.js";
|
|
2
|
+
/** Pure current-row check. Ordering matches admission semantics: identity before mutable grants. */
|
|
3
|
+
export function rosterRefusalFor({ record, call }) {
|
|
4
|
+
if (call.origin === undefined || call.origin.kind === "preset")
|
|
5
|
+
return "unverifiable";
|
|
6
|
+
if (record === null)
|
|
7
|
+
return "removed";
|
|
8
|
+
if (record.admissionRevision !== call.origin.admissionRevision)
|
|
9
|
+
return "changed";
|
|
10
|
+
if (!record.enabled)
|
|
11
|
+
return "turned-off";
|
|
12
|
+
if (record.disconnected)
|
|
13
|
+
return "disconnected";
|
|
14
|
+
const refusal = refusalForAdmittedToolUnderCurrentGrants({ tool: call, grants: record.grants });
|
|
15
|
+
return refusal === null ? null : refusal === "remote-declares-not-read-only" ? "write-not-allowed" : "tool-not-allowed";
|
|
16
|
+
}
|
|
17
|
+
/** Revocation half of a connection gate; credential refresh/reauth checks are composed by the host.
|
|
18
|
+
* Presets have no roster row. Reads and notification failures never disclose driver errors.
|
|
19
|
+
*/
|
|
20
|
+
export function createFederatedConnectionRevocationGate(required, optional = {}) {
|
|
21
|
+
const report = (diagnostic) => {
|
|
22
|
+
try {
|
|
23
|
+
optional.onDiagnostic?.(diagnostic);
|
|
24
|
+
}
|
|
25
|
+
catch { /* Diagnostics cannot bypass refusal. */ }
|
|
26
|
+
};
|
|
27
|
+
return async ({ connectionId, call }) => {
|
|
28
|
+
if (call.origin?.kind === "preset")
|
|
29
|
+
return;
|
|
30
|
+
let record = null;
|
|
31
|
+
let reason;
|
|
32
|
+
try {
|
|
33
|
+
record = await required.roster.findByServerId({ serverId: connectionId }, optional.scope === undefined ? {} : { scope: optional.scope });
|
|
34
|
+
reason = rosterRefusalFor({ record, call });
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
report({ connectionId, stage: "store", error });
|
|
38
|
+
reason = "unverifiable";
|
|
39
|
+
}
|
|
40
|
+
if (reason === null)
|
|
41
|
+
return;
|
|
42
|
+
const refusal = { serverId: connectionId, label: record?.label ?? null, remoteName: call.remoteName, reason };
|
|
43
|
+
try {
|
|
44
|
+
await optional.webhooks?.connectionRefused(refusal, optional.scope === undefined ? {} : { scope: optional.scope });
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
report({ connectionId, stage: "webhook", error });
|
|
48
|
+
}
|
|
49
|
+
throw required.errorFactory.create(refusal);
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=revocation.js.map
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { type Logger } from "@jini-ai/core/primitives";
|
|
2
|
+
import type { ToolRegistry } from "@jini-ai/core";
|
|
3
|
+
import type { ResolvedFederatedConnection } from "./config.js";
|
|
4
|
+
import type { McpSessionPort } from "./ports.js";
|
|
5
|
+
import { type FederationDeps } from "./registrations.js";
|
|
6
|
+
import type { FederatedAdmissionReport } from "./trust.js";
|
|
7
|
+
export interface AttachFederatedToolsResult {
|
|
8
|
+
/** Ids actually registered. Empty when federation is off or every attempt failed. */
|
|
9
|
+
readonly registeredToolIds: readonly string[];
|
|
10
|
+
/** Live sessions, for the caller to close at shutdown. */
|
|
11
|
+
readonly sessions: readonly McpSessionPort[];
|
|
12
|
+
/**
|
|
13
|
+
* One entry per connection that reached admission — i.e. every connection whose session
|
|
14
|
+
* connected and listed tools, whether or not any tool was ultimately registered. A connection
|
|
15
|
+
* that failed before `federateSession` ran (a bad spawn, a timed-out handshake, a collision) has
|
|
16
|
+
* no admission decision to report and contributes no entry here — see
|
|
17
|
+
* {@link attachOneFederatedConnection}'s catch branch.
|
|
18
|
+
*
|
|
19
|
+
* This is the same accounting `logFederatedAdmissionReport` already turns into stderr lines and
|
|
20
|
+
* then discards. Kept here so a caller (today, `agent-daemon-server.ts`, over `GET
|
|
21
|
+
* /api/federation/admissions`) can serve it to an operator instead of it only ever reaching
|
|
22
|
+
* whoever happens to be tailing the daemon's terminal at boot. See the write-tools implementation
|
|
23
|
+
* outline,.
|
|
24
|
+
* See docs/decisions/DR-001-federation-admission.md.
|
|
25
|
+
*/
|
|
26
|
+
readonly reports: readonly {
|
|
27
|
+
readonly connectionId: string;
|
|
28
|
+
readonly report: FederatedAdmissionReport;
|
|
29
|
+
/**
|
|
30
|
+
* Whether this connection came from `presets.ts`'s registry rather than the operator-editable
|
|
31
|
+
* roster (`extraConnections` below). Threaded through so a consumer — today, the admin admissions
|
|
32
|
+
* banner (`external-mcp-admissions-rules.ts`) — can tell "no roster card because this is a preset,
|
|
33
|
+
* by design" apart from "no roster card because the operator just deleted it". Both looked
|
|
34
|
+
* identical before this field existed (`connectionId` absent from the roster either way), which
|
|
35
|
+
* is exactly why a genuinely-deleted-but-still-live roster connection was silently reported as
|
|
36
|
+
* agreeing instead of as drift (2026-09-07, the follow-on ADM-001 left open).
|
|
37
|
+
*/
|
|
38
|
+
readonly isPreset: boolean;
|
|
39
|
+
}[];
|
|
40
|
+
/**
|
|
41
|
+
* One entry per connection that failed BEFORE reaching admission — a bad spawn, a timed-out
|
|
42
|
+
* handshake, a malformed tool listing, or a native-id collision (2026-09-24). This is the
|
|
43
|
+
* connection-level counterpart to `reports` above: `reports` never carries an entry for one of
|
|
44
|
+
* these (see {@link attachOneFederatedConnection}'s catch branch), which used to mean the
|
|
45
|
+
* connection was simply invisible everywhere an operator could look — `GET
|
|
46
|
+
* /api/federation/admissions` showed no row for it and `configFailures` (a SEPARATE, boot-time
|
|
47
|
+
* config-resolution channel — see `external-mcp-connection-source.ts`) has no way to know about a
|
|
48
|
+
* failure that happens deeper, inside `connect()` itself. `reason` is `messageOf(error)` — the
|
|
49
|
+
* same text `logger.warn` already prints for this exact failure, so this field discloses nothing
|
|
50
|
+
* that was not already reaching this process's own stderr; it is never a raw env value or secret,
|
|
51
|
+
* since nothing in this file's `connect`/`spawn` error paths ever formats one into a message.
|
|
52
|
+
*/
|
|
53
|
+
readonly connectFailures: readonly {
|
|
54
|
+
readonly connectionId: string;
|
|
55
|
+
readonly reason: string;
|
|
56
|
+
}[];
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Connects every configured federated MCP server, registers whatever clears the trust tier into
|
|
60
|
+
* `registry`, and returns what happened.
|
|
61
|
+
*
|
|
62
|
+
* Must be awaited BEFORE `buildToolCatalogQuery(registry)` runs: that function snapshots
|
|
63
|
+
* `registry.list({})` into an FTS index once, so a tool registered afterwards would be executable but
|
|
64
|
+
* invisible to `search_tools`/`describe_tool`.
|
|
65
|
+
*
|
|
66
|
+
* @param params.registry - The daemon's registry, already populated with the native catalog.
|
|
67
|
+
* @param params.deps - `permissionGate` + an optional opaque `scope`, the slice federated handlers gate against.
|
|
68
|
+
* @param params.connections - Defaults to whatever the presets registered with `presets.ts` resolve
|
|
69
|
+
* from the environment.
|
|
70
|
+
* @param params.connect - Session factory, injected so tests substitute a double for the real
|
|
71
|
+
* `spawn` + handshake.
|
|
72
|
+
* @returns The registered ids, the open sessions, and one admission report per connection that
|
|
73
|
+
* reached admission. Never rejects.
|
|
74
|
+
* @complexity O(c · t) in connections and their advertised tools.
|
|
75
|
+
*/
|
|
76
|
+
export interface AttachFederatedMcpToolsParams {
|
|
77
|
+
registry: ToolRegistry;
|
|
78
|
+
deps: FederationDeps;
|
|
79
|
+
connections?: readonly ResolvedFederatedConnection[] | undefined;
|
|
80
|
+
/**
|
|
81
|
+
* Connections from a config source that is not the preset registry — today, the operator-editable
|
|
82
|
+
* roster in `assistant/external-mcp-store.ts`.
|
|
83
|
+
*
|
|
84
|
+
* A second parameter rather than a second preset, because `FederatedMcpPresetResolver` cannot
|
|
85
|
+
* express this source: it is synchronous (a DB read plus an unseal are not) and returns at most
|
|
86
|
+
* one connection (a roster returns N). Appended AFTER the presets so a preset keeps first claim on
|
|
87
|
+
* a contested tool id under R1, which preserves the existing behaviour of every already-configured
|
|
88
|
+
* deployment — an operator adding a row cannot displace a vendor preset that was already working.
|
|
89
|
+
*/
|
|
90
|
+
extraConnections?: readonly ResolvedFederatedConnection[] | undefined;
|
|
91
|
+
connect: (request: {
|
|
92
|
+
connection: ResolvedFederatedConnection;
|
|
93
|
+
}) => Promise<McpSessionPort>;
|
|
94
|
+
logger?: Logger | undefined;
|
|
95
|
+
env?: NodeJS.ProcessEnv | undefined;
|
|
96
|
+
}
|
|
97
|
+
export type AttachFederatedMcpToolsRequiredArgs = Pick<AttachFederatedMcpToolsParams, "registry" | "deps" | "connect">;
|
|
98
|
+
export type AttachFederatedMcpToolsOptions = Omit<AttachFederatedMcpToolsParams, "registry" | "deps" | "connect">;
|
|
99
|
+
export declare function attachFederatedMcpTools(requiredArgs: AttachFederatedMcpToolsRequiredArgs, optionalArgs?: AttachFederatedMcpToolsOptions): Promise<AttachFederatedToolsResult>;
|
|
100
|
+
//# sourceMappingURL=bootstrap.d.ts.map
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { createConsoleLogger } from "@jini-ai/core/primitives";
|
|
2
|
+
import { listFederatedMcpPresets } from "./presets.js";
|
|
3
|
+
import { federateSession } from "./registrations.js";
|
|
4
|
+
/**
|
|
5
|
+
* @file The composition root for outbound MCP federation: the one function
|
|
6
|
+
* `agent-daemon-server.ts` calls, and the only place in this subtree that performs I/O it was not
|
|
7
|
+
* handed.
|
|
8
|
+
*
|
|
9
|
+
* Split out of `agent-daemon-server.ts` for the reason `tool-catalog-query.ts` and `daemon-auth.ts`
|
|
10
|
+
* were: that file is a top-level side-effecting script which opens a real port and a real DB
|
|
11
|
+
* connection on import, so nothing in it can be imported by a test. Everything here can be.
|
|
12
|
+
*
|
|
13
|
+
* FAIL-OPEN, and deliberately the opposite of `daemon-auth.ts`'s fail-closed gate. The two are
|
|
14
|
+
* answering different questions. `requireAgentDaemonToken` guards ACCESS TO host app, where failing
|
|
15
|
+
* open would admit unauthenticated callers, so an unconfigured gate must refuse to serve. This
|
|
16
|
+
* guards an OPTIONAL OUTBOUND CONVENIENCE, where failing closed would mean a third party's server
|
|
17
|
+
* being slow, broken, or absent takes host app's own assistant down with it. A vendor host app does not
|
|
18
|
+
* control must never be on the critical path of host app booting. So every failure here — unreachable
|
|
19
|
+
* server, timed-out handshake, malformed tool list, invalid config — is logged and stepped over,
|
|
20
|
+
* and the daemon continues with its native catalog exactly as it did before this capability
|
|
21
|
+
* existed.
|
|
22
|
+
*
|
|
23
|
+
* The one thing that is NOT stepped over is a native id collision (`trust.ts` R1): that throws out
|
|
24
|
+
* of `federateSession` and is caught here like any other failure, so the connection is dropped
|
|
25
|
+
* whole rather than partially registered. Dropping the connection is the safe direction — the
|
|
26
|
+
* failure mode it prevents is a remote shadowing a host app tool, and "no federated tools" is always an
|
|
27
|
+
* acceptable outcome.
|
|
28
|
+
*
|
|
29
|
+
* VENDOR-BLIND. Nothing in this file names a vendor: which servers exist is whatever registered
|
|
30
|
+
* itself with `presets.ts` (none today: the Supabase env preset was retired on 2026-09-29), plus the
|
|
31
|
+
* stored roster. Before 2026-07-30 this file imported
|
|
32
|
+
* Supabase's resolver directly, which meant adding a second vendor was an edit to core federation.
|
|
33
|
+
* See `presets.ts` for the seam's rationale.
|
|
34
|
+
*/
|
|
35
|
+
/** Where the admission report goes. Injected so tests assert on it instead of scraping stdout, and
|
|
36
|
+
* so a future structured logger is a parameter change rather than an edit. */
|
|
37
|
+
const consoleLogger = createConsoleLogger({ prefix: "agent-daemon" });
|
|
38
|
+
/** Resolves the defaulted inputs `attachFederatedMcpTools` needs — `logger`, `connect`, and the
|
|
39
|
+
* merged connection list (presets or an injected override, plus any extra roster connections),
|
|
40
|
+
* each tagged with its origin. Split out purely to keep that function under the shop complexity
|
|
41
|
+
* ceiling.
|
|
42
|
+
*
|
|
43
|
+
* `params.connections` (an injected override, used throughout this file's own tests) stands in for
|
|
44
|
+
* the preset list, not the roster — it replaces `resolveRegisteredPresets`'s result, and
|
|
45
|
+
* `extraConnections` is documented on {@link AttachFederatedMcpToolsParams} as specifically the
|
|
46
|
+
* operator-editable roster. So the origin tag is exactly which of the two arrays a connection came
|
|
47
|
+
* from, unchanged from before this field existed. */
|
|
48
|
+
/** Stamps a preset-sourced connection's config with `origin: {kind:"preset"}`, so
|
|
49
|
+
* `external-mcp-revocation.ts`'s per-call gate can tell it apart from a roster connection (which
|
|
50
|
+
* carries its OWN origin already, stamped by `external-mcp-store.ts`'s
|
|
51
|
+
* `toResolvedFederatedConnections`) and skip the row re-check a preset has no row to support. Split
|
|
52
|
+
* out purely to keep {@link resolveFederationAttachInputs} under the shop complexity ceiling. */
|
|
53
|
+
function withPresetOrigin(connection) {
|
|
54
|
+
return { ...connection, config: { ...connection.config, origin: { kind: "preset" } } };
|
|
55
|
+
}
|
|
56
|
+
function resolveFederationAttachInputs(params) {
|
|
57
|
+
const logger = params.logger ?? consoleLogger;
|
|
58
|
+
const connect = params.connect;
|
|
59
|
+
const presetConnections = params.connections ?? resolveRegisteredPresets(params.env ?? process.env, logger);
|
|
60
|
+
const connections = [
|
|
61
|
+
...presetConnections.map((connection) => ({ connection: withPresetOrigin(connection), isPreset: true })),
|
|
62
|
+
...(params.extraConnections ?? []).map((connection) => ({ connection, isPreset: false })),
|
|
63
|
+
];
|
|
64
|
+
return { logger, connect, connections };
|
|
65
|
+
}
|
|
66
|
+
export async function attachFederatedMcpTools(requiredArgs, optionalArgs = {}) {
|
|
67
|
+
const params = { ...requiredArgs, ...optionalArgs };
|
|
68
|
+
const { logger, connect, connections } = resolveFederationAttachInputs(params);
|
|
69
|
+
if (connections.length === 0)
|
|
70
|
+
return { registeredToolIds: [], sessions: [], reports: [], connectFailures: [] };
|
|
71
|
+
const registeredToolIds = [];
|
|
72
|
+
const sessions = [];
|
|
73
|
+
const reports = [];
|
|
74
|
+
const connectFailures = [];
|
|
75
|
+
for (const { connection, isPreset } of connections) {
|
|
76
|
+
const attached = await attachOneFederatedConnection({ connection, registry: params.registry, deps: params.deps, connect, logger });
|
|
77
|
+
registeredToolIds.push(...attached.registeredToolIds);
|
|
78
|
+
if (attached.session)
|
|
79
|
+
sessions.push(attached.session);
|
|
80
|
+
if (attached.report)
|
|
81
|
+
reports.push({ connectionId: connection.config.connectionId, report: attached.report, isPreset });
|
|
82
|
+
if (attached.connectFailureReason !== undefined) {
|
|
83
|
+
connectFailures.push({ connectionId: connection.config.connectionId, reason: attached.connectFailureReason });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return { registeredToolIds, sessions, reports, connectFailures };
|
|
87
|
+
}
|
|
88
|
+
/** Registers every admitted tool from one connection's {@link federateSession} pass into
|
|
89
|
+
* `registry`, returning the ids actually registered. Split out of
|
|
90
|
+
* {@link attachOneFederatedConnection} purely to keep that function's complexity under the shop
|
|
91
|
+
* ceiling. */
|
|
92
|
+
function registerFederatedTools(registry, registrations) {
|
|
93
|
+
const registeredToolIds = [];
|
|
94
|
+
for (const registration of registrations) {
|
|
95
|
+
registry.register(registration);
|
|
96
|
+
registeredToolIds.push(registration.descriptor.id);
|
|
97
|
+
}
|
|
98
|
+
return registeredToolIds;
|
|
99
|
+
}
|
|
100
|
+
/** Logs one connection's full admission accounting. Refusals are reported, never silent —
|
|
101
|
+
* `buildDomainRegistrations`'s own "silence is never the outcome" discipline. An operator
|
|
102
|
+
* debugging a missing tool needs the reason, and an operator reading logs after an incident needs
|
|
103
|
+
* to see what a remote TRIED to expose. Split out of {@link attachOneFederatedConnection} purely
|
|
104
|
+
* to keep that function's complexity under the shop ceiling.
|
|
105
|
+
*
|
|
106
|
+
* The two `writeAuthorized`/`writeAllowedButNotAllowlisted` lines are WARN, not INFO, on purpose:
|
|
107
|
+
* an operator-authorized write tool entering the model's catalog — or an operator's write
|
|
108
|
+
* authorization silently doing nothing because the same name is missing from the allowlist — is a
|
|
109
|
+
* security-relevant boot event, not routine informational noise. See the write-tools
|
|
110
|
+
* implementation outline §9. */
|
|
111
|
+
function logFederatedAdmissionReport(connectionId, report, logger) {
|
|
112
|
+
for (const refusal of report.refused) {
|
|
113
|
+
logger.warn({ message: `mcp-federation: '${connectionId}' refused remote tool '${refusal.remoteName}' — ${refusal.reason}` });
|
|
114
|
+
}
|
|
115
|
+
for (const admitted of report.admitted) {
|
|
116
|
+
if (admitted.writeAuthorized) {
|
|
117
|
+
logger.warn({ message: `mcp-federation: '${connectionId}' admitted WRITE tool '${admitted.remoteName}' (operator-authorized)` });
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
for (const absent of report.allowlistedButAbsent) {
|
|
121
|
+
logger.warn({ message: `mcp-federation: '${connectionId}' allowlists '${absent}' but the server never advertised it — check the allowlist for a typo, or the server's --features` });
|
|
122
|
+
}
|
|
123
|
+
for (const drift of report.writeAllowedButNotAllowlisted) {
|
|
124
|
+
logger.warn({ message: `mcp-federation: '${connectionId}' write-authorizes '${drift}' but it is not in the allowlist — it will never be admitted until it is added to both` });
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/** One iteration of {@link attachFederatedMcpTools}'s original inline loop body — connect, list,
|
|
128
|
+
* admit, register, log — extracted purely to keep that function's complexity under the shop
|
|
129
|
+
* ceiling. Fail-open per connection: any failure (connect, list, or admission) is logged and
|
|
130
|
+
* swallowed here rather than propagated, matching this file's own fail-open doc. A session that
|
|
131
|
+
* connected but failed later still gets closed. */
|
|
132
|
+
async function attachOneFederatedConnection(params) {
|
|
133
|
+
const { connection, registry, deps, connect, logger } = params;
|
|
134
|
+
const { connectionId } = connection.config;
|
|
135
|
+
let session;
|
|
136
|
+
try {
|
|
137
|
+
session = await connect({ connection });
|
|
138
|
+
// Snapshotted here, immediately before the admission check, so the collision assertion sees
|
|
139
|
+
// every native tool AND every tool an earlier connection in this same loop already claimed.
|
|
140
|
+
const nativeToolIds = new Set(registry.list({}).map((descriptor) => descriptor.id));
|
|
141
|
+
const { registrations, report } = await federateSession({ session, config: connection.config, deps, nativeToolIds });
|
|
142
|
+
const registeredToolIds = registerFederatedTools(registry, registrations);
|
|
143
|
+
logger.info({ message: `mcp-federation: '${connectionId}' registered ${registrations.length} federated tool(s): ${registrations.map((r) => r.descriptor.id).join(", ") || "(none)"}` });
|
|
144
|
+
logFederatedAdmissionReport(connectionId, report, logger);
|
|
145
|
+
return { registeredToolIds, session, report };
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
logger.warn({ message: `mcp-federation: '${connectionId}' failed, continuing without its tools — ${messageOf(error)}` });
|
|
149
|
+
// A session that connected but failed during listing/admission still owns a child process.
|
|
150
|
+
await session?.close({}).catch(() => undefined);
|
|
151
|
+
return { registeredToolIds: [], session: null, report: null, connectFailureReason: messageOf(error) };
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Asks every registered preset for a connection, in registration order.
|
|
156
|
+
*
|
|
157
|
+
* Errors are isolated PER PRESET rather than abandoning the whole resolution pass: an enabled-but-
|
|
158
|
+
* invalid Supabase config must not also disable a correctly-configured second vendor that happens to
|
|
159
|
+
* be registered after it. Each failure is loud — the operator meant to have that connection and does
|
|
160
|
+
* not — and still non-fatal, because host app's own assistant is unaffected either way.
|
|
161
|
+
*
|
|
162
|
+
* A preset that declines (`null`) is silent: "not configured" is the expected default state, and
|
|
163
|
+
* logging it every boot would train operators to ignore this channel.
|
|
164
|
+
*/
|
|
165
|
+
function resolveRegisteredPresets(env, logger) {
|
|
166
|
+
const connections = [];
|
|
167
|
+
for (const preset of listFederatedMcpPresets()) {
|
|
168
|
+
try {
|
|
169
|
+
const connection = preset.resolve({ env: env });
|
|
170
|
+
if (connection)
|
|
171
|
+
connections.push(connection);
|
|
172
|
+
}
|
|
173
|
+
catch (error) {
|
|
174
|
+
logger.warn({ message: `mcp-federation: preset '${preset.presetId}' configuration is invalid, continuing without federated tools — ${messageOf(error)}` });
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return connections;
|
|
178
|
+
}
|
|
179
|
+
function messageOf(error) {
|
|
180
|
+
return error instanceof Error ? error.message : String(error);
|
|
181
|
+
}
|
|
182
|
+
//# sourceMappingURL=bootstrap.js.map
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { FederatedMcpConnectionConfig, McpLaunchSpec } from "./ports.js";
|
|
2
|
+
/**
|
|
3
|
+
* @file GENERIC configuration-resolution scaffolding for federated MCP connections — the parts every
|
|
4
|
+
* vendor preset needs and none of which names a vendor.
|
|
5
|
+
*
|
|
6
|
+
* What this file deliberately does NOT contain, as of 2026-07-30: any specific vendor's preset. The
|
|
7
|
+
* Supabase preset this capability was designed and verified against used to live here; it moved to
|
|
8
|
+
* `src/features/plugins/supabase-mcp/` on 2026-07-30 and was retired on 2026-09-29 (Supabase is now
|
|
9
|
+
* the `content/agent-plugins/supabase` Agent Plugin). The reasoning is the same one that keeps `store-plugin.ts` and `deploy-plugin.ts` out
|
|
10
|
+
* of `src/server` proper: a concrete vendor integration that ships by default is still an OPTIONAL
|
|
11
|
+
* module, and a reader who finds it inside `src/assistant/` reasonably concludes the assistant
|
|
12
|
+
* REQUIRES it. Nothing in `mcp-federation/` should be readable as "host app's assistant needs Supabase".
|
|
13
|
+
*
|
|
14
|
+
* What stays here is the scaffolding a SECOND preset would otherwise have to duplicate: the
|
|
15
|
+
* resolved-connection shape, the shared timeout/size ceilings, and the three env-parsing helpers
|
|
16
|
+
* (enabled-flag, comma list, positive int). Their semantics are the load-bearing part — see each
|
|
17
|
+
* one's own comment — and having exactly one implementation of them is what makes two presets behave
|
|
18
|
+
* the same way about the same kind of setting.
|
|
19
|
+
*
|
|
20
|
+
* Architectural role:
|
|
21
|
+
* Configuration resolution. Pure apart from reading an injected env bag. No I/O.
|
|
22
|
+
*/
|
|
23
|
+
/**
|
|
24
|
+
* One fully-resolved federated connection: how to reach it, and the policy to hold it to.
|
|
25
|
+
*
|
|
26
|
+
* This is the whole contract between a vendor preset and core federation. A preset's entire job is
|
|
27
|
+
* to produce one of these (or `null`); everything downstream — `presets.ts`, `bootstrap.ts`,
|
|
28
|
+
* `trust.ts`, `registrations.ts` — is vendor-blind and works off this shape alone.
|
|
29
|
+
*
|
|
30
|
+
* `launch` is TRANSPORT-BLIND too: a local command or a hosted endpoint, and every policy field
|
|
31
|
+
* beside it applies identically to both. That is what lets an operator move a connection from a
|
|
32
|
+
* locally-launched server to the vendor's hosted one without any of the trust machinery noticing.
|
|
33
|
+
*/
|
|
34
|
+
export interface ResolvedFederatedConnection {
|
|
35
|
+
readonly config: FederatedMcpConnectionConfig;
|
|
36
|
+
readonly launch: McpLaunchSpec;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Shared ceilings, so two presets do not disagree about what a reasonable bound is.
|
|
40
|
+
*
|
|
41
|
+
* A preset may override any of them when it has a vendor-specific reason (and should say why); the
|
|
42
|
+
* point of a shared default is that "no particular reason" resolves to the same number everywhere.
|
|
43
|
+
*/
|
|
44
|
+
export declare const FEDERATED_CONNECTION_DEFAULTS: {
|
|
45
|
+
readonly connectTimeoutMs: 60000;
|
|
46
|
+
readonly callTimeoutMs: 30000;
|
|
47
|
+
readonly maxResultBytes: number;
|
|
48
|
+
readonly maxTools: 32;
|
|
49
|
+
};
|
|
50
|
+
/** `1`/`true`/`yes`/`on`, case-insensitive. Anything else — including unset — is off, because the
|
|
51
|
+
* default for connecting an assistant to a third party must be "no". */
|
|
52
|
+
export declare function isFederationEnabled({ value }: {
|
|
53
|
+
value: string | undefined;
|
|
54
|
+
}): boolean;
|
|
55
|
+
/** `undefined` (use the preset's default) is distinguished from `""` (an operator explicitly
|
|
56
|
+
* allowing nothing), so setting the variable empty is a working way to disable every tool without
|
|
57
|
+
* unsetting the connection. */
|
|
58
|
+
export declare function parseAllowedToolNames({ value }: {
|
|
59
|
+
value: string | undefined;
|
|
60
|
+
}): string[] | null;
|
|
61
|
+
/** Malformed and non-positive values fall back rather than throw: a mistyped timeout should not be
|
|
62
|
+
* the difference between having federation and not, whereas a mistyped credential or project scope
|
|
63
|
+
* (which a preset validates itself) should be. */
|
|
64
|
+
export declare function positiveIntOrDefault({ value, fallback }: {
|
|
65
|
+
value: string | undefined;
|
|
66
|
+
fallback: number;
|
|
67
|
+
}): number;
|
|
68
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared ceilings, so two presets do not disagree about what a reasonable bound is.
|
|
3
|
+
*
|
|
4
|
+
* A preset may override any of them when it has a vendor-specific reason (and should say why); the
|
|
5
|
+
* point of a shared default is that "no particular reason" resolves to the same number everywhere.
|
|
6
|
+
*/
|
|
7
|
+
export const FEDERATED_CONNECTION_DEFAULTS = {
|
|
8
|
+
connectTimeoutMs: 60_000,
|
|
9
|
+
callTimeoutMs: 30_000,
|
|
10
|
+
maxResultBytes: 64 * 1024,
|
|
11
|
+
maxTools: 32,
|
|
12
|
+
};
|
|
13
|
+
/** `1`/`true`/`yes`/`on`, case-insensitive. Anything else — including unset — is off, because the
|
|
14
|
+
* default for connecting an assistant to a third party must be "no". */
|
|
15
|
+
export function isFederationEnabled({ value }) {
|
|
16
|
+
if (typeof value !== "string")
|
|
17
|
+
return false;
|
|
18
|
+
return ["1", "true", "yes", "on"].includes(value.trim().toLowerCase());
|
|
19
|
+
}
|
|
20
|
+
/** `undefined` (use the preset's default) is distinguished from `""` (an operator explicitly
|
|
21
|
+
* allowing nothing), so setting the variable empty is a working way to disable every tool without
|
|
22
|
+
* unsetting the connection. */
|
|
23
|
+
export function parseAllowedToolNames({ value }) {
|
|
24
|
+
if (typeof value !== "string")
|
|
25
|
+
return null;
|
|
26
|
+
return value
|
|
27
|
+
.split(",")
|
|
28
|
+
.map((entry) => entry.trim())
|
|
29
|
+
.filter((entry) => entry.length > 0);
|
|
30
|
+
}
|
|
31
|
+
/** Malformed and non-positive values fall back rather than throw: a mistyped timeout should not be
|
|
32
|
+
* the difference between having federation and not, whereas a mistyped credential or project scope
|
|
33
|
+
* (which a preset validates itself) should be. */
|
|
34
|
+
export function positiveIntOrDefault({ value, fallback }) {
|
|
35
|
+
if (typeof value !== "string")
|
|
36
|
+
return fallback;
|
|
37
|
+
const parsed = Number.parseInt(value, 10);
|
|
38
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export * from "./ports.js";
|
|
2
|
+
export * from "./mcp-protocol.js";
|
|
3
|
+
export * from "./adapter.http.js";
|
|
4
|
+
export * from "./config.js";
|
|
5
|
+
export * from "./presets.js";
|
|
6
|
+
export * from "./trust.js";
|
|
7
|
+
export * from "./refusal-notice.js";
|
|
8
|
+
export * from "./registrations.js";
|
|
9
|
+
export * from "./bootstrap.js";
|
|
10
|
+
export * from "./reload.js";
|
|
11
|
+
export * from "./tool-approvals.js";
|
|
12
|
+
export * from "./approvals/index.js";
|
|
13
|
+
export * from "./messages.js";
|
|
14
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export * from "./ports.js";
|
|
2
|
+
export * from "./mcp-protocol.js";
|
|
3
|
+
export * from "./adapter.http.js";
|
|
4
|
+
export * from "./config.js";
|
|
5
|
+
export * from "./presets.js";
|
|
6
|
+
export * from "./trust.js";
|
|
7
|
+
export * from "./refusal-notice.js";
|
|
8
|
+
export * from "./registrations.js";
|
|
9
|
+
export * from "./bootstrap.js";
|
|
10
|
+
export * from "./reload.js";
|
|
11
|
+
export * from "./tool-approvals.js";
|
|
12
|
+
export * from "./approvals/index.js";
|
|
13
|
+
export * from "./messages.js";
|
|
14
|
+
//# sourceMappingURL=index.js.map
|