@decocms/bindings 1.2.0 → 1.3.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/package.json +7 -2
- package/src/core/client/mcp-client.ts +1 -1
- package/src/core/client/proxy.ts +17 -2
- package/src/core/plugin-context-provider.tsx +5 -72
- package/src/core/plugin-router.tsx +12 -7
- package/src/core/plugins.ts +19 -8
- package/src/core/server-plugin.ts +120 -3
- package/src/index.ts +3 -0
- package/src/well-known/workflow.ts +6 -0
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decocms/bindings",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"check": "tsc --noEmit",
|
|
7
7
|
"test": "bun test"
|
|
8
8
|
},
|
|
9
9
|
"dependencies": {
|
|
10
|
-
"@modelcontextprotocol/sdk": "1.
|
|
10
|
+
"@modelcontextprotocol/sdk": "1.26.0",
|
|
11
11
|
"@tanstack/react-router": "1.139.7",
|
|
12
12
|
"react": "^19.2.0",
|
|
13
13
|
"zod": "^4.0.0",
|
|
@@ -35,6 +35,11 @@
|
|
|
35
35
|
"engines": {
|
|
36
36
|
"node": ">=24.0.0"
|
|
37
37
|
},
|
|
38
|
+
"repository": {
|
|
39
|
+
"type": "git",
|
|
40
|
+
"url": "git+https://github.com/decocms/mesh.git",
|
|
41
|
+
"directory": "packages/bindings"
|
|
42
|
+
},
|
|
38
43
|
"publishConfig": {
|
|
39
44
|
"access": "public"
|
|
40
45
|
}
|
package/src/core/client/proxy.ts
CHANGED
|
@@ -77,7 +77,12 @@ export function createMCPClientProxy<T extends Record<string, unknown>>(
|
|
|
77
77
|
const { client, callStreamableTool } = await createClient(extraHeaders);
|
|
78
78
|
|
|
79
79
|
if (options?.streamable?.[String(toolName)]) {
|
|
80
|
-
|
|
80
|
+
if (!callStreamableTool) {
|
|
81
|
+
throw new Error(
|
|
82
|
+
`Tool ${String(toolName)} requires streaming support but client doesn't provide callStreamableTool`,
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
return await callStreamableTool(String(toolName), args);
|
|
81
86
|
}
|
|
82
87
|
|
|
83
88
|
const { structuredContent, isError, content } = await client.callTool({
|
|
@@ -111,7 +116,17 @@ export function createMCPClientProxy<T extends Record<string, unknown>>(
|
|
|
111
116
|
)}`,
|
|
112
117
|
);
|
|
113
118
|
}
|
|
114
|
-
|
|
119
|
+
|
|
120
|
+
// Prefer structuredContent, but fall back to parsing content[0].text
|
|
121
|
+
// structuredContent may be undefined if the response doesn't include it
|
|
122
|
+
// (e.g., SDK version mismatch, schema parsing stripping unknown fields)
|
|
123
|
+
if (structuredContent !== undefined) {
|
|
124
|
+
return structuredContent;
|
|
125
|
+
}
|
|
126
|
+
const textContent = (content as { text: string }[])?.[0]?.text;
|
|
127
|
+
return typeof textContent === "string"
|
|
128
|
+
? safeParse(textContent)
|
|
129
|
+
: undefined;
|
|
115
130
|
}
|
|
116
131
|
|
|
117
132
|
async function listToolsFn() {
|
|
@@ -1,37 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Plugin Context Provider
|
|
2
|
+
* Plugin Context Provider Types
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* The runtime implementations (PluginContextProvider, usePluginContext)
|
|
5
|
+
* have moved to @decocms/mesh-sdk/plugins. Only types remain here
|
|
6
|
+
* for backwards compatibility.
|
|
5
7
|
*/
|
|
6
8
|
|
|
7
|
-
import { createContext, useContext, type ReactNode } from "react";
|
|
8
9
|
import type { Binder } from "./binder";
|
|
9
10
|
import type { PluginContext, PluginContextPartial } from "./plugin-context";
|
|
10
|
-
|
|
11
|
-
// Internal context stores the partial version (nullable connection fields)
|
|
12
|
-
// The hook return type depends on the options passed
|
|
13
|
-
const PluginContextInternal = createContext<PluginContextPartial | null>(null);
|
|
11
|
+
import type { ReactNode } from "react";
|
|
14
12
|
|
|
15
13
|
export interface PluginContextProviderProps<TBinding extends Binder> {
|
|
16
14
|
value: PluginContext<TBinding> | PluginContextPartial<TBinding>;
|
|
17
15
|
children: ReactNode;
|
|
18
16
|
}
|
|
19
17
|
|
|
20
|
-
/**
|
|
21
|
-
* Provider component for plugin context.
|
|
22
|
-
* Used by the mesh app layout to provide context to plugin routes.
|
|
23
|
-
*/
|
|
24
|
-
export function PluginContextProvider<TBinding extends Binder>({
|
|
25
|
-
value,
|
|
26
|
-
children,
|
|
27
|
-
}: PluginContextProviderProps<TBinding>) {
|
|
28
|
-
return (
|
|
29
|
-
<PluginContextInternal.Provider value={value as PluginContextPartial}>
|
|
30
|
-
{children}
|
|
31
|
-
</PluginContextInternal.Provider>
|
|
32
|
-
);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
18
|
/**
|
|
36
19
|
* Options for usePluginContext hook.
|
|
37
20
|
*/
|
|
@@ -42,53 +25,3 @@ export interface UsePluginContextOptions {
|
|
|
42
25
|
*/
|
|
43
26
|
partial?: boolean;
|
|
44
27
|
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Hook to access the plugin context with typed tool caller.
|
|
48
|
-
*
|
|
49
|
-
* @template TBinding - The binding type for typed tool calls
|
|
50
|
-
* @param options - Optional settings
|
|
51
|
-
* @param options.partial - Set to true in empty state components where connection may not exist
|
|
52
|
-
* @throws Error if used outside of PluginContextProvider
|
|
53
|
-
* @throws Error if connection is null but partial option is not set
|
|
54
|
-
*
|
|
55
|
-
* @example
|
|
56
|
-
* ```tsx
|
|
57
|
-
* // In route component (connection guaranteed by layout)
|
|
58
|
-
* const { toolCaller, connection } = usePluginContext<typeof REGISTRY_APP_BINDING>();
|
|
59
|
-
* const result = await toolCaller("COLLECTION_REGISTRY_APP_LIST", { limit: 20 });
|
|
60
|
-
*
|
|
61
|
-
* // In empty state component (no connection available)
|
|
62
|
-
* const { session, org } = usePluginContext<typeof REGISTRY_APP_BINDING>({ partial: true });
|
|
63
|
-
* ```
|
|
64
|
-
*/
|
|
65
|
-
export function usePluginContext<TBinding extends Binder = Binder>(options: {
|
|
66
|
-
partial: true;
|
|
67
|
-
}): PluginContextPartial<TBinding>;
|
|
68
|
-
export function usePluginContext<
|
|
69
|
-
TBinding extends Binder = Binder,
|
|
70
|
-
>(): PluginContext<TBinding>;
|
|
71
|
-
export function usePluginContext<TBinding extends Binder = Binder>(
|
|
72
|
-
options?: UsePluginContextOptions,
|
|
73
|
-
): PluginContext<TBinding> | PluginContextPartial<TBinding> {
|
|
74
|
-
const context = useContext(PluginContextInternal);
|
|
75
|
-
if (!context) {
|
|
76
|
-
throw new Error(
|
|
77
|
-
"usePluginContext must be used within a PluginContextProvider",
|
|
78
|
-
);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// If partial mode, return as-is with nullable fields
|
|
82
|
-
if (options?.partial) {
|
|
83
|
-
return context as PluginContextPartial<TBinding>;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// Otherwise, assert that connection exists (routes should always have one)
|
|
87
|
-
if (!context.connectionId || !context.connection || !context.toolCaller) {
|
|
88
|
-
throw new Error(
|
|
89
|
-
"usePluginContext requires a valid connection. Use { partial: true } in empty state components.",
|
|
90
|
-
);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
return context as PluginContext<TBinding>;
|
|
94
|
-
}
|
|
@@ -17,19 +17,20 @@ import {
|
|
|
17
17
|
import type { PluginSetupContext } from "./plugins";
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
|
-
* Prepends the plugin base path (/$org/$pluginId) to a route path.
|
|
20
|
+
* Prepends the plugin base path (/$org/$project/$pluginId) to a route path.
|
|
21
21
|
* Handles both absolute plugin paths (starting with /) and relative paths.
|
|
22
22
|
*/
|
|
23
23
|
function prependBasePath(
|
|
24
24
|
to: string | undefined,
|
|
25
25
|
org: string,
|
|
26
|
+
project: string,
|
|
26
27
|
pluginId: string,
|
|
27
28
|
): string {
|
|
28
|
-
if (!to) return `/${org}/${pluginId}`;
|
|
29
|
+
if (!to) return `/${org}/${project}/${pluginId}`;
|
|
29
30
|
|
|
30
31
|
// If path starts with /, it's relative to the plugin root
|
|
31
32
|
if (to.startsWith("/")) {
|
|
32
|
-
return `/${org}/${pluginId}${to}`;
|
|
33
|
+
return `/${org}/${project}/${pluginId}${to}`;
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
// Otherwise, it's already a full path or relative
|
|
@@ -122,8 +123,9 @@ export function createPluginRouter<TRoutes extends AnyRoute | AnyRoute[]>(
|
|
|
122
123
|
*/
|
|
123
124
|
useNavigate: () => {
|
|
124
125
|
const navigate = useNavigate();
|
|
125
|
-
const { org, pluginId } = useParams({ strict: false }) as {
|
|
126
|
+
const { org, project, pluginId } = useParams({ strict: false }) as {
|
|
126
127
|
org: string;
|
|
128
|
+
project: string;
|
|
127
129
|
pluginId: string;
|
|
128
130
|
};
|
|
129
131
|
|
|
@@ -134,13 +136,14 @@ export function createPluginRouter<TRoutes extends AnyRoute | AnyRoute[]>(
|
|
|
134
136
|
search?: TRouteById<TTo>["types"]["fullSearchSchema"];
|
|
135
137
|
},
|
|
136
138
|
) => {
|
|
137
|
-
const to = prependBasePath(options.to, org, pluginId);
|
|
139
|
+
const to = prependBasePath(options.to, org, project, pluginId);
|
|
138
140
|
|
|
139
141
|
return navigate({
|
|
140
142
|
...options,
|
|
141
143
|
to,
|
|
142
144
|
params: {
|
|
143
145
|
org,
|
|
146
|
+
project,
|
|
144
147
|
pluginId,
|
|
145
148
|
...(options.params as Record<string, string>),
|
|
146
149
|
},
|
|
@@ -168,12 +171,13 @@ export function createPluginRouter<TRoutes extends AnyRoute | AnyRoute[]>(
|
|
|
168
171
|
children?: ReactNode;
|
|
169
172
|
},
|
|
170
173
|
) {
|
|
171
|
-
const { org, pluginId } = useParams({ strict: false }) as {
|
|
174
|
+
const { org, project, pluginId } = useParams({ strict: false }) as {
|
|
172
175
|
org: string;
|
|
176
|
+
project: string;
|
|
173
177
|
pluginId: string;
|
|
174
178
|
};
|
|
175
179
|
|
|
176
|
-
const to = prependBasePath(props.to as string, org, pluginId);
|
|
180
|
+
const to = prependBasePath(props.to as string, org, project, pluginId);
|
|
177
181
|
|
|
178
182
|
return (
|
|
179
183
|
<TanStackLink
|
|
@@ -181,6 +185,7 @@ export function createPluginRouter<TRoutes extends AnyRoute | AnyRoute[]>(
|
|
|
181
185
|
to={to}
|
|
182
186
|
params={{
|
|
183
187
|
org,
|
|
188
|
+
project,
|
|
184
189
|
pluginId,
|
|
185
190
|
...props.params,
|
|
186
191
|
}}
|
package/src/core/plugins.ts
CHANGED
|
@@ -18,6 +18,13 @@ export interface RegisterRootSidebarItemParams {
|
|
|
18
18
|
label: string;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
export interface RegisterSidebarGroupParams {
|
|
22
|
+
id: string;
|
|
23
|
+
label: string;
|
|
24
|
+
items: RegisterRootSidebarItemParams[];
|
|
25
|
+
defaultExpanded?: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
21
28
|
export interface RegisterEmptyStateParams {
|
|
22
29
|
component: ReactNode;
|
|
23
30
|
}
|
|
@@ -29,6 +36,7 @@ export interface PluginSetupContext {
|
|
|
29
36
|
lazyRouteComponent: typeof lazyRouteComponent;
|
|
30
37
|
};
|
|
31
38
|
registerRootSidebarItem: (params: RegisterRootSidebarItemParams) => void;
|
|
39
|
+
registerSidebarGroup: (params: RegisterSidebarGroupParams) => void;
|
|
32
40
|
registerPluginRoutes: (route: AnyRoute[]) => void;
|
|
33
41
|
}
|
|
34
42
|
|
|
@@ -50,13 +58,17 @@ export interface PluginRenderHeaderProps {
|
|
|
50
58
|
* Client plugins are separate from server plugins to avoid bundling
|
|
51
59
|
* server code into the client bundle.
|
|
52
60
|
*/
|
|
53
|
-
export interface ClientPlugin<TBinding extends Binder> {
|
|
61
|
+
export interface ClientPlugin<TBinding extends Binder = Binder> {
|
|
54
62
|
id: string;
|
|
55
63
|
/**
|
|
56
64
|
* Short description of the plugin shown in the settings UI.
|
|
57
65
|
*/
|
|
58
66
|
description?: string;
|
|
59
|
-
|
|
67
|
+
/**
|
|
68
|
+
* Binding schema used to filter compatible connections.
|
|
69
|
+
* Omit for plugins that manage their own connection (e.g. self MCP).
|
|
70
|
+
*/
|
|
71
|
+
binding?: TBinding;
|
|
60
72
|
setup?: PluginSetup;
|
|
61
73
|
/**
|
|
62
74
|
* Optional custom layout component for this plugin.
|
|
@@ -96,10 +108,9 @@ export {
|
|
|
96
108
|
type RouteById,
|
|
97
109
|
} from "./plugin-router";
|
|
98
110
|
|
|
99
|
-
//
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
type UsePluginContextOptions,
|
|
111
|
+
// Note: PluginContextProvider and usePluginContext have been moved to @decocms/mesh-sdk/plugins.
|
|
112
|
+
// Types are re-exported here for backwards compatibility.
|
|
113
|
+
export type {
|
|
114
|
+
PluginContextProviderProps,
|
|
115
|
+
UsePluginContextOptions,
|
|
105
116
|
} from "./plugin-context-provider";
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* - API routes (authenticated and public)
|
|
7
7
|
* - Database migrations
|
|
8
8
|
* - Storage factories
|
|
9
|
+
* - Event handlers (via the event bus)
|
|
9
10
|
*
|
|
10
11
|
* Server plugins are separate from client plugins to avoid bundling
|
|
11
12
|
* server code into the client bundle.
|
|
@@ -15,15 +16,45 @@ import type { Hono } from "hono";
|
|
|
15
16
|
import type { Kysely } from "kysely";
|
|
16
17
|
|
|
17
18
|
/**
|
|
18
|
-
*
|
|
19
|
-
*
|
|
19
|
+
* Subset of MeshContext exposed to server plugin tool handlers.
|
|
20
|
+
*
|
|
21
|
+
* Plugins receive the full MeshContext at runtime but should only depend on
|
|
22
|
+
* these properties. This keeps the plugin contract stable and avoids coupling
|
|
23
|
+
* plugins to Mesh internals (db, vault, tracer, etc.).
|
|
24
|
+
*/
|
|
25
|
+
export interface ServerPluginToolContext {
|
|
26
|
+
organization: { id: string } | null;
|
|
27
|
+
access: { check: () => Promise<void> };
|
|
28
|
+
auth: {
|
|
29
|
+
user?: { id: string; email?: string; name?: string };
|
|
30
|
+
};
|
|
31
|
+
/** Kysely database instance for direct queries. */
|
|
32
|
+
db: Kysely<unknown>;
|
|
33
|
+
createMCPProxy: (connectionId: string) => Promise<{
|
|
34
|
+
callTool: (args: {
|
|
35
|
+
name: string;
|
|
36
|
+
arguments?: Record<string, unknown>;
|
|
37
|
+
}) => Promise<{
|
|
38
|
+
isError?: boolean;
|
|
39
|
+
content?: Array<{ type?: string; text?: string }>;
|
|
40
|
+
structuredContent?: unknown;
|
|
41
|
+
}>;
|
|
42
|
+
listTools: () => Promise<{
|
|
43
|
+
tools: Array<{ name: string; description?: string }>;
|
|
44
|
+
}>;
|
|
45
|
+
close?: () => Promise<void>;
|
|
46
|
+
}>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Tool definition for server plugins.
|
|
20
51
|
*/
|
|
21
52
|
export interface ServerPluginToolDefinition {
|
|
22
53
|
name: string;
|
|
23
54
|
description?: string;
|
|
24
55
|
inputSchema: unknown;
|
|
25
56
|
outputSchema?: unknown;
|
|
26
|
-
handler: (input: unknown, ctx:
|
|
57
|
+
handler: (input: unknown, ctx: ServerPluginToolContext) => Promise<unknown>;
|
|
27
58
|
}
|
|
28
59
|
|
|
29
60
|
/**
|
|
@@ -51,6 +82,63 @@ export interface ServerPluginContext {
|
|
|
51
82
|
};
|
|
52
83
|
}
|
|
53
84
|
|
|
85
|
+
/**
|
|
86
|
+
* Event handler context provided to plugin event handlers.
|
|
87
|
+
* Contains the organization ID and a publish function for emitting follow-up events.
|
|
88
|
+
*/
|
|
89
|
+
export interface ServerPluginEventContext {
|
|
90
|
+
/** Organization ID the events belong to */
|
|
91
|
+
organizationId: string;
|
|
92
|
+
/** Connection ID of the SELF MCP for this organization */
|
|
93
|
+
connectionId: string;
|
|
94
|
+
/** Publish a follow-up event to the event bus */
|
|
95
|
+
publish: (
|
|
96
|
+
type: string,
|
|
97
|
+
subject: string,
|
|
98
|
+
data?: Record<string, unknown>,
|
|
99
|
+
options?: { deliverAt?: string },
|
|
100
|
+
) => Promise<void>;
|
|
101
|
+
/** Create an MCP proxy client for calling tools on a connection */
|
|
102
|
+
createMCPProxy: (connectionId: string) => Promise<{
|
|
103
|
+
callTool: (
|
|
104
|
+
params: { name: string; arguments?: Record<string, unknown> },
|
|
105
|
+
resultSchema?: unknown,
|
|
106
|
+
options?: { timeout?: number },
|
|
107
|
+
) => Promise<{
|
|
108
|
+
content?: unknown;
|
|
109
|
+
structuredContent?: unknown;
|
|
110
|
+
isError?: boolean;
|
|
111
|
+
}>;
|
|
112
|
+
close: () => Promise<void>;
|
|
113
|
+
}>;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Startup context provided to plugin onStartup hooks.
|
|
118
|
+
* Contains the database and a publish function for emitting recovery events.
|
|
119
|
+
*/
|
|
120
|
+
export interface ServerPluginStartupContext {
|
|
121
|
+
/** Database instance */
|
|
122
|
+
db: Kysely<unknown>;
|
|
123
|
+
/** Publish an event to the event bus for a given organization */
|
|
124
|
+
publish: (
|
|
125
|
+
organizationId: string,
|
|
126
|
+
event: { type: string; subject: string; data?: Record<string, unknown> },
|
|
127
|
+
) => Promise<void>;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Event definition for a CloudEvent received by a plugin.
|
|
132
|
+
*/
|
|
133
|
+
export interface ServerPluginEvent {
|
|
134
|
+
id: string;
|
|
135
|
+
type: string;
|
|
136
|
+
source: string;
|
|
137
|
+
subject?: string;
|
|
138
|
+
data?: unknown;
|
|
139
|
+
time?: string;
|
|
140
|
+
}
|
|
141
|
+
|
|
54
142
|
/**
|
|
55
143
|
* Server Plugin interface.
|
|
56
144
|
*
|
|
@@ -95,6 +183,35 @@ export interface ServerPlugin {
|
|
|
95
183
|
* Called during context initialization.
|
|
96
184
|
*/
|
|
97
185
|
createStorage?: (ctx: ServerPluginContext) => unknown;
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Event handler for this plugin.
|
|
189
|
+
*
|
|
190
|
+
* When defined, the system will:
|
|
191
|
+
* 1. Auto-subscribe the SELF connection to the specified event types per-organization
|
|
192
|
+
* 2. Route matching events from the event bus to this handler
|
|
193
|
+
*
|
|
194
|
+
* Events are durable (persisted in the event bus) with at-least-once delivery.
|
|
195
|
+
* The handler receives batches of events and a context for publishing follow-up events.
|
|
196
|
+
*/
|
|
197
|
+
onEvents?: {
|
|
198
|
+
/** Event type patterns this plugin handles (e.g., "workflow.execution.created") */
|
|
199
|
+
types: string[];
|
|
200
|
+
/** Handle a batch of events. Errors are logged but don't affect other plugins. */
|
|
201
|
+
handler: (
|
|
202
|
+
events: ServerPluginEvent[],
|
|
203
|
+
ctx: ServerPluginEventContext,
|
|
204
|
+
) => Promise<void> | void;
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Startup hook called once after the event bus is ready.
|
|
209
|
+
*
|
|
210
|
+
* Use this to recover from crashes (e.g., resume stuck workflow executions).
|
|
211
|
+
* Called after storage is initialized and the event bus worker has started.
|
|
212
|
+
* Errors are logged but don't prevent other plugins from starting.
|
|
213
|
+
*/
|
|
214
|
+
onStartup?: (ctx: ServerPluginStartupContext) => Promise<void>;
|
|
98
215
|
}
|
|
99
216
|
|
|
100
217
|
/**
|
package/src/index.ts
CHANGED
|
@@ -79,6 +79,12 @@ export const StepConfigSchema = z.object({
|
|
|
79
79
|
.number()
|
|
80
80
|
.optional()
|
|
81
81
|
.describe("Max execution time in ms before step fails (default: 30000)"),
|
|
82
|
+
onError: z
|
|
83
|
+
.enum(["fail", "continue"])
|
|
84
|
+
.optional()
|
|
85
|
+
.describe(
|
|
86
|
+
"What to do when this step fails: 'fail' aborts the workflow, 'continue' skips the error and proceeds",
|
|
87
|
+
),
|
|
82
88
|
});
|
|
83
89
|
export type StepConfig = z.infer<typeof StepConfigSchema>;
|
|
84
90
|
|