@decocms/bindings 1.0.8 → 1.0.9

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 CHANGED
@@ -1,13 +1,15 @@
1
1
  {
2
2
  "name": "@decocms/bindings",
3
- "version": "1.0.8",
3
+ "version": "1.0.9",
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.25.1",
10
+ "@modelcontextprotocol/sdk": "1.25.2",
11
+ "@tanstack/react-router": "1.139.7",
12
+ "react": "^19.2.0",
11
13
  "zod": "^4.0.0",
12
14
  "zod-from-json-schema": "^0.5.2"
13
15
  },
@@ -20,7 +22,9 @@
20
22
  "./mcp": "./src/well-known/mcp.ts",
21
23
  "./assistant": "./src/well-known/assistant.ts",
22
24
  "./prompt": "./src/well-known/prompt.ts",
23
- "./workflow": "./src/well-known/workflow.ts"
25
+ "./workflow": "./src/well-known/workflow.ts",
26
+ "./plugins": "./src/core/plugins.ts",
27
+ "./plugin-router": "./src/core/plugin-router.tsx"
24
28
  },
25
29
  "engines": {
26
30
  "node": ">=24.0.0"
@@ -177,3 +177,46 @@ export function createBindingChecker<TDefinition extends readonly ToolBinder[]>(
177
177
  },
178
178
  };
179
179
  }
180
+
181
+ /**
182
+ * Generic connection type for binding checking.
183
+ * Any connection object with a tools array can be checked.
184
+ */
185
+ export interface ConnectionForBinding {
186
+ tools?: Array<{
187
+ name: string;
188
+ inputSchema?: Record<string, unknown>;
189
+ outputSchema?: Record<string, unknown>;
190
+ }> | null;
191
+ }
192
+
193
+ /**
194
+ * Checks if a connection implements a binding by validating its tools.
195
+ * Used by plugin layouts to filter connections by binding.
196
+ */
197
+ export function connectionImplementsBinding(
198
+ connection: ConnectionForBinding,
199
+ binding: Binder,
200
+ ): boolean {
201
+ const tools = connection.tools;
202
+
203
+ if (!tools || tools.length === 0) {
204
+ return false;
205
+ }
206
+
207
+ // Prepare tools for checker (only input schema, skip output for detection)
208
+ const toolsForChecker = tools.map((t) => ({
209
+ name: t.name,
210
+ inputSchema: t.inputSchema,
211
+ }));
212
+
213
+ // Create binding checker without output schemas
214
+ const bindingForChecker = binding.map((b) => ({
215
+ name: b.name,
216
+ inputSchema: b.inputSchema,
217
+ opt: b.opt,
218
+ }));
219
+
220
+ const checker = createBindingChecker(bindingForChecker);
221
+ return checker.isImplementedBy(toolsForChecker);
222
+ }
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Plugin Context Provider
3
+ *
4
+ * React context provider and hook for accessing plugin context.
5
+ */
6
+
7
+ import { createContext, useContext, type ReactNode } from "react";
8
+ import type { Binder } from "./binder";
9
+ 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);
14
+
15
+ export interface PluginContextProviderProps<TBinding extends Binder> {
16
+ value: PluginContext<TBinding> | PluginContextPartial<TBinding>;
17
+ children: ReactNode;
18
+ }
19
+
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
+ /**
36
+ * Options for usePluginContext hook.
37
+ */
38
+ export interface UsePluginContextOptions {
39
+ /**
40
+ * Set to true when calling from an empty state component.
41
+ * This returns nullable connection fields since no valid connection exists.
42
+ */
43
+ partial?: boolean;
44
+ }
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
+ }
@@ -0,0 +1,183 @@
1
+ /**
2
+ * Plugin Context Types
3
+ *
4
+ * Provides typed context for plugins to access their selected connection
5
+ * and call tools with full type safety based on the plugin's binding.
6
+ */
7
+
8
+ import type { Binder, ToolBinder } from "./binder";
9
+ import type { z } from "zod";
10
+
11
+ /**
12
+ * Connection entity shape provided by the layout.
13
+ */
14
+ export interface PluginConnectionEntity {
15
+ id: string;
16
+ title: string;
17
+ icon: string | null;
18
+ description: string | null;
19
+ app_name: string | null;
20
+ app_id: string | null;
21
+ tools: Array<{
22
+ name: string;
23
+ description?: string;
24
+ inputSchema?: Record<string, unknown>;
25
+ outputSchema?: Record<string, unknown>;
26
+ }> | null;
27
+ metadata: Record<string, unknown> | null;
28
+ }
29
+
30
+ /**
31
+ * Organization context.
32
+ */
33
+ export interface PluginOrgContext {
34
+ id: string;
35
+ slug: string;
36
+ name: string;
37
+ }
38
+
39
+ /**
40
+ * User session provided to plugins.
41
+ */
42
+ export interface PluginSession {
43
+ user: {
44
+ id: string;
45
+ name: string;
46
+ email: string;
47
+ image?: string | null;
48
+ };
49
+ }
50
+
51
+ /**
52
+ * Helper type to extract tool by name from a binding.
53
+ */
54
+ type ExtractToolByName<TBinding extends Binder, TName extends string> = Extract<
55
+ TBinding[number],
56
+ { name: TName }
57
+ >;
58
+
59
+ /**
60
+ * Typed tool caller for a specific binding.
61
+ * Provides type-safe tool calls based on the binding definition.
62
+ *
63
+ * @template TBinding - The binding type to derive tool types from
64
+ */
65
+ export type TypedToolCaller<TBinding extends Binder> = <
66
+ TName extends TBinding[number]["name"] & string,
67
+ >(
68
+ toolName: TName,
69
+ args: ExtractToolByName<TBinding, TName> extends ToolBinder<
70
+ TName,
71
+ infer TInput,
72
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
73
+ infer _TOutput
74
+ >
75
+ ? TInput extends z.ZodType
76
+ ? z.infer<TInput>
77
+ : TInput
78
+ : unknown,
79
+ ) => Promise<
80
+ ExtractToolByName<TBinding, TName> extends ToolBinder<
81
+ TName,
82
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
83
+ infer _TInput,
84
+ infer TOutput
85
+ >
86
+ ? TOutput extends z.ZodType
87
+ ? z.infer<TOutput>
88
+ : TOutput
89
+ : unknown
90
+ >;
91
+
92
+ /**
93
+ * Base plugin context with connection fields always available.
94
+ * Used by plugin routes where layout guarantees a valid connection.
95
+ *
96
+ * @template TBinding - The binding type the plugin requires
97
+ *
98
+ * @example
99
+ * ```tsx
100
+ * // In plugin route component (connection guaranteed)
101
+ * const { toolCaller, connection } = usePluginContext<typeof REGISTRY_APP_BINDING>();
102
+ *
103
+ * // toolCaller and connection are non-null
104
+ * const result = await toolCaller("COLLECTION_REGISTRY_APP_LIST", { limit: 20 });
105
+ * ```
106
+ */
107
+ export interface PluginContext<TBinding extends Binder = Binder> {
108
+ /**
109
+ * The selected connection ID.
110
+ * Always defined in routes (layout handles empty state separately).
111
+ */
112
+ connectionId: string;
113
+
114
+ /**
115
+ * The selected connection entity.
116
+ * Always defined in routes.
117
+ */
118
+ connection: PluginConnectionEntity;
119
+
120
+ /**
121
+ * Typed tool caller for the selected connection.
122
+ * Call MCP tools with full type safety based on the plugin's binding.
123
+ */
124
+ toolCaller: TypedToolCaller<TBinding>;
125
+
126
+ /**
127
+ * Organization context.
128
+ * Always available.
129
+ */
130
+ org: PluginOrgContext;
131
+
132
+ /**
133
+ * Current user session.
134
+ * Available when user is authenticated.
135
+ */
136
+ session: PluginSession | null;
137
+ }
138
+
139
+ /**
140
+ * Partial plugin context with nullable connection fields.
141
+ * Used by empty state components where no valid connection exists.
142
+ *
143
+ * @template TBinding - The binding type the plugin requires
144
+ *
145
+ * @example
146
+ * ```tsx
147
+ * // In empty state component (no connection)
148
+ * const { session, org } = usePluginContext<typeof REGISTRY_APP_BINDING>({ partial: true });
149
+ *
150
+ * // connection, connectionId, toolCaller are null
151
+ * ```
152
+ */
153
+ export interface PluginContextPartial<TBinding extends Binder = Binder> {
154
+ /**
155
+ * The selected connection ID.
156
+ * Null when no valid connection is available.
157
+ */
158
+ connectionId: string | null;
159
+
160
+ /**
161
+ * The selected connection entity.
162
+ * Null when no valid connection is available.
163
+ */
164
+ connection: PluginConnectionEntity | null;
165
+
166
+ /**
167
+ * Typed tool caller for the selected connection.
168
+ * Null when no valid connection is available.
169
+ */
170
+ toolCaller: TypedToolCaller<TBinding> | null;
171
+
172
+ /**
173
+ * Organization context.
174
+ * Always available.
175
+ */
176
+ org: PluginOrgContext;
177
+
178
+ /**
179
+ * Current user session.
180
+ * Available when user is authenticated.
181
+ */
182
+ session: PluginSession | null;
183
+ }
@@ -0,0 +1,226 @@
1
+ import type { ReactNode } from "react";
2
+ import {
3
+ createRoute,
4
+ lazyRouteComponent,
5
+ Route,
6
+ useNavigate,
7
+ useParams,
8
+ useSearch,
9
+ useLocation,
10
+ Link as TanStackLink,
11
+ type AnyRoute,
12
+ type RouteIds,
13
+ type RouteById,
14
+ type LinkProps,
15
+ type NavigateOptions,
16
+ } from "@tanstack/react-router";
17
+ import type { PluginSetupContext } from "./plugins";
18
+
19
+ /**
20
+ * Prepends the plugin base path (/$org/$pluginId) to a route path.
21
+ * Handles both absolute plugin paths (starting with /) and relative paths.
22
+ */
23
+ function prependBasePath(
24
+ to: string | undefined,
25
+ org: string,
26
+ pluginId: string,
27
+ ): string {
28
+ if (!to) return `/${org}/${pluginId}`;
29
+
30
+ // If path starts with /, it's relative to the plugin root
31
+ if (to.startsWith("/")) {
32
+ return `/${org}/${pluginId}${to}`;
33
+ }
34
+
35
+ // Otherwise, it's already a full path or relative
36
+ return to;
37
+ }
38
+
39
+ /**
40
+ * Creates a typed plugin router from a route factory function.
41
+ *
42
+ * Routes are registered directly under ctx.parentRoute (which already has
43
+ * component: Outlet). Return an array of sibling routes for multiple pages.
44
+ *
45
+ * @example
46
+ * ```tsx
47
+ * export const storeRouter = createPluginRouter((ctx) => {
48
+ * const indexRoute = ctx.routing.createRoute({
49
+ * getParentRoute: () => ctx.parentRoute,
50
+ * path: "/",
51
+ * component: ctx.routing.lazyRouteComponent(() => import("./routes/page.tsx")),
52
+ * });
53
+ *
54
+ * const detailRoute = ctx.routing.createRoute({
55
+ * getParentRoute: () => ctx.parentRoute,
56
+ * path: "/$appName",
57
+ * component: ctx.routing.lazyRouteComponent(() => import("./routes/detail.tsx")),
58
+ * validateSearch: z.object({ tab: z.string().optional() }),
59
+ * });
60
+ *
61
+ * return [indexRoute, detailRoute];
62
+ * });
63
+ *
64
+ * // In plugin setup - register each route
65
+ * export const storePlugin: AnyPlugin = {
66
+ * id: "store",
67
+ * setup: (ctx) => {
68
+ * const routes = storeRouter.createRoutes(ctx);
69
+ * for (const route of routes) {
70
+ * ctx.registerRootPluginRoute(route);
71
+ * }
72
+ * },
73
+ * };
74
+ *
75
+ * // In components
76
+ * function DetailPage() {
77
+ * const { appName } = storeRouter.useParams({ from: "/$appName" });
78
+ * const { tab } = storeRouter.useSearch({ from: "/$appName" });
79
+ *
80
+ * const navigate = storeRouter.useNavigate();
81
+ * navigate({ to: "/$appName", params: { appName: "other" } });
82
+ * }
83
+ * ```
84
+ */
85
+ export function createPluginRouter<TRoutes extends AnyRoute | AnyRoute[]>(
86
+ createRoutes: (ctx: PluginSetupContext) => TRoutes,
87
+ ) {
88
+ // Extract route type from array or single route
89
+ type TRoute = TRoutes extends (infer R)[] ? R : TRoutes;
90
+ type TRouteId = TRoute extends AnyRoute ? RouteIds<TRoute> : never;
91
+ type TRouteById<TId extends TRouteId> = TRoute extends AnyRoute
92
+ ? RouteById<TRoute, TId>
93
+ : never;
94
+
95
+ return {
96
+ /**
97
+ * Create the route tree. Call this in your plugin's setup().
98
+ */
99
+ createRoutes,
100
+
101
+ /**
102
+ * Get route params with TanStack's type inference.
103
+ */
104
+ useParams: <TFrom extends TRouteId>(_options: { from: TFrom }) => {
105
+ return useParams({
106
+ strict: false,
107
+ }) as TRouteById<TFrom>["types"]["allParams"];
108
+ },
109
+
110
+ /**
111
+ * Get search params with TanStack's type inference.
112
+ */
113
+ useSearch: <TFrom extends TRouteId>(_options: { from: TFrom }) => {
114
+ return useSearch({
115
+ strict: false,
116
+ }) as TRouteById<TFrom>["types"]["fullSearchSchema"];
117
+ },
118
+
119
+ /**
120
+ * Navigate within the plugin.
121
+ * Automatically prepends /$org/$pluginId to the path.
122
+ */
123
+ useNavigate: () => {
124
+ const navigate = useNavigate();
125
+ const { org, pluginId } = useParams({ strict: false }) as {
126
+ org: string;
127
+ pluginId: string;
128
+ };
129
+
130
+ return <TTo extends TRouteId>(
131
+ options: Omit<NavigateOptions, "to" | "params"> & {
132
+ to: TTo;
133
+ params?: TRouteById<TTo>["types"]["allParams"];
134
+ search?: TRouteById<TTo>["types"]["fullSearchSchema"];
135
+ },
136
+ ) => {
137
+ const to = prependBasePath(options.to, org, pluginId);
138
+
139
+ return navigate({
140
+ ...options,
141
+ to,
142
+ params: {
143
+ org,
144
+ pluginId,
145
+ ...(options.params as Record<string, string>),
146
+ },
147
+ } as NavigateOptions);
148
+ };
149
+ },
150
+
151
+ /**
152
+ * Get the current location.
153
+ */
154
+ useLocation: () => {
155
+ return useLocation();
156
+ },
157
+
158
+ /**
159
+ * Link component for plugin navigation.
160
+ * Automatically prepends /$org/$pluginId to the path.
161
+ */
162
+ Link: function PluginLink<TTo extends TRouteId>(
163
+ props: Omit<LinkProps, "to" | "params" | "search"> & {
164
+ to: TTo;
165
+ params?: TRouteById<TTo>["types"]["allParams"];
166
+ search?: TRouteById<TTo>["types"]["fullSearchSchema"];
167
+ className?: string;
168
+ children?: ReactNode;
169
+ },
170
+ ) {
171
+ const { org, pluginId } = useParams({ strict: false }) as {
172
+ org: string;
173
+ pluginId: string;
174
+ };
175
+
176
+ const to = prependBasePath(props.to as string, org, pluginId);
177
+
178
+ return (
179
+ <TanStackLink
180
+ {...(props as LinkProps)}
181
+ to={to}
182
+ params={{
183
+ org,
184
+ pluginId,
185
+ ...props.params,
186
+ }}
187
+ />
188
+ );
189
+ },
190
+
191
+ /**
192
+ * Type helpers
193
+ */
194
+ _types: {
195
+ routes: undefined as unknown as TRoutes,
196
+ routeIds: undefined as unknown as TRouteId,
197
+ },
198
+ };
199
+ }
200
+
201
+ /**
202
+ * Type helper to extract route IDs from a plugin router
203
+ */
204
+ export type PluginRouteIds<
205
+ TRouter extends ReturnType<typeof createPluginRouter>,
206
+ > = TRouter["_types"]["routeIds"];
207
+
208
+ /**
209
+ * Type helper to extract routes from a plugin router
210
+ */
211
+ export type PluginRoutes<
212
+ TRouter extends ReturnType<typeof createPluginRouter>,
213
+ > = TRouter["_types"]["routes"];
214
+
215
+ // Re-export TanStack utilities for plugins
216
+ export {
217
+ createRoute,
218
+ lazyRouteComponent,
219
+ Route,
220
+ TanStackLink as Link,
221
+ useNavigate,
222
+ useParams,
223
+ useSearch,
224
+ useLocation,
225
+ };
226
+ export type { AnyRoute, RouteIds, RouteById };
@@ -0,0 +1,81 @@
1
+ import {
2
+ createRoute,
3
+ lazyRouteComponent,
4
+ type AnyRoute,
5
+ } from "@tanstack/react-router";
6
+ import { Binder } from "./binder";
7
+ import type { ReactNode } from "react";
8
+ import type { PluginConnectionEntity } from "./plugin-context";
9
+
10
+ export interface ToolViewItem {
11
+ toolName: string;
12
+ label: string;
13
+ icon: ReactNode;
14
+ }
15
+
16
+ export interface RegisterRootSidebarItemParams {
17
+ icon: ReactNode;
18
+ label: string;
19
+ }
20
+
21
+ export interface RegisterEmptyStateParams {
22
+ component: ReactNode;
23
+ }
24
+
25
+ export interface PluginSetupContext {
26
+ parentRoute: AnyRoute;
27
+ routing: {
28
+ createRoute: typeof createRoute;
29
+ lazyRouteComponent: typeof lazyRouteComponent;
30
+ };
31
+ registerRootSidebarItem: (params: RegisterRootSidebarItemParams) => void;
32
+ registerPluginRoutes: (route: AnyRoute[]) => void;
33
+ }
34
+
35
+ export type PluginSetup = (context: PluginSetupContext) => void;
36
+
37
+ /**
38
+ * Props passed to plugin's renderHeader function.
39
+ */
40
+ export interface PluginRenderHeaderProps {
41
+ connections: PluginConnectionEntity[];
42
+ selectedConnectionId: string;
43
+ onConnectionChange: (connectionId: string) => void;
44
+ }
45
+
46
+ export interface Plugin<TBinding extends Binder> {
47
+ id: string;
48
+ /**
49
+ * Short description of the plugin shown in the settings UI.
50
+ */
51
+ description?: string;
52
+ binding: TBinding;
53
+ setup: PluginSetup;
54
+ /**
55
+ * Optional custom layout component for this plugin.
56
+ * If not provided, a default layout with connection selector will be used.
57
+ * @deprecated Use renderHeader and renderEmptyState instead.
58
+ */
59
+ LayoutComponent?: React.ComponentType;
60
+ /**
61
+ * Render the header with connection selector.
62
+ * Receives the list of valid connections and current selection handlers.
63
+ */
64
+ renderHeader?: (props: PluginRenderHeaderProps) => ReactNode;
65
+ /**
66
+ * Render the empty state when no valid connections are available.
67
+ */
68
+ renderEmptyState?: () => ReactNode;
69
+ }
70
+
71
+ export type AnyPlugin = Plugin<any>;
72
+
73
+ // Re-export plugin router utilities
74
+ export {
75
+ createPluginRouter,
76
+ type PluginRouteIds,
77
+ type PluginRoutes,
78
+ type AnyRoute,
79
+ type RouteIds,
80
+ type RouteById,
81
+ } from "./plugin-router";
package/src/index.ts CHANGED
@@ -8,12 +8,32 @@
8
8
  // Re-export core binder types and utilities
9
9
  export {
10
10
  createBindingChecker,
11
+ bindingClient,
12
+ connectionImplementsBinding,
11
13
  type Binder,
12
14
  type BindingChecker,
13
15
  type ToolBinder,
14
16
  type ToolWithSchemas,
17
+ type ConnectionForBinding,
15
18
  } from "./core/binder";
16
19
 
20
+ // Re-export plugin context types and provider
21
+ export {
22
+ type PluginContext,
23
+ type PluginContextPartial,
24
+ type PluginConnectionEntity,
25
+ type PluginOrgContext,
26
+ type PluginSession,
27
+ type TypedToolCaller,
28
+ } from "./core/plugin-context";
29
+
30
+ export {
31
+ PluginContextProvider,
32
+ usePluginContext,
33
+ type PluginContextProviderProps,
34
+ type UsePluginContextOptions,
35
+ } from "./core/plugin-context-provider";
36
+
17
37
  // Re-export registry binding types
18
38
  export {
19
39
  MCPRegistryServerSchema,
@@ -70,3 +90,21 @@ export {
70
90
  EventBusBinding,
71
91
  type EventBusBindingClient,
72
92
  } from "./well-known/event-bus";
93
+
94
+ // Re-export object storage binding types
95
+ export {
96
+ OBJECT_STORAGE_BINDING,
97
+ type ObjectStorageBinding,
98
+ type ListObjectsInput,
99
+ type ListObjectsOutput,
100
+ type GetObjectMetadataInput,
101
+ type GetObjectMetadataOutput,
102
+ type GetPresignedUrlInput,
103
+ type GetPresignedUrlOutput,
104
+ type PutPresignedUrlInput,
105
+ type PutPresignedUrlOutput,
106
+ type DeleteObjectInput,
107
+ type DeleteObjectOutput,
108
+ type DeleteObjectsInput,
109
+ type DeleteObjectsOutput,
110
+ } from "./well-known/object-storage";
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Defines the interface for AI assistant providers.
5
5
  * Any MCP that implements this binding can provide configurable AI assistants
6
- * with a system prompt and runtime configuration (gateway + model).
6
+ * with a system prompt and runtime configuration (virtual MCP + model).
7
7
  *
8
8
  * This binding uses collection bindings for full CRUD operations.
9
9
  */
@@ -37,10 +37,12 @@ export const AssistantSchema = BaseCollectionEntitySchema.extend({
37
37
  .describe("System prompt that defines the assistant's behavior"),
38
38
 
39
39
  /**
40
- * Selected gateway for this assistant (single gateway).
41
- * This gateway determines which MCP tools are exposed to chat.
40
+ * Selected virtual MCP (agent) for this assistant.
41
+ * This virtual MCP determines which MCP tools are exposed to chat.
42
42
  */
43
- gateway_id: z.string().describe("Gateway ID to use for this assistant"),
43
+ virtual_mcp_id: z
44
+ .string()
45
+ .describe("Virtual MCP ID to use for this assistant"),
44
46
 
45
47
  /**
46
48
  * Selected model for this assistant (model id + the connection where it lives).
@@ -75,7 +77,7 @@ export const ASSISTANTS_COLLECTION_BINDING = createCollectionBindings(
75
77
  *
76
78
  * Required tools:
77
79
  * - COLLECTION_ASSISTANT_LIST: List available AI assistants with their configurations
78
- * - COLLECTION_ASSISTANT_GET: Get a single assistant by ID (includes system_prompt, gateway_id, model)
80
+ * - COLLECTION_ASSISTANT_GET: Get a single assistant by ID (includes system_prompt, virtual_mcp_id, model)
79
81
  *
80
82
  * Optional tools:
81
83
  * - COLLECTION_ASSISTANT_CREATE: Create a new assistant
@@ -150,10 +150,10 @@ export const EventSubscribeOutputSchema = z.object({
150
150
  enabled: z.boolean().describe("Whether subscription is enabled"),
151
151
 
152
152
  /** Created timestamp */
153
- createdAt: z.union([z.string(), z.date()]).describe("Created timestamp"),
153
+ createdAt: z.string().datetime().describe("Created timestamp (ISO 8601)"),
154
154
 
155
155
  /** Updated timestamp */
156
- updatedAt: z.union([z.string(), z.date()]).describe("Updated timestamp"),
156
+ updatedAt: z.string().datetime().describe("Updated timestamp (ISO 8601)"),
157
157
  }),
158
158
  });
159
159
 
@@ -209,10 +209,10 @@ export const SubscriptionDetailSchema = z.object({
209
209
  enabled: z.boolean().describe("Whether subscription is enabled"),
210
210
 
211
211
  /** Created timestamp */
212
- createdAt: z.union([z.string(), z.date()]).describe("Created timestamp"),
212
+ createdAt: z.string().datetime().describe("Created timestamp (ISO 8601)"),
213
213
 
214
214
  /** Updated timestamp */
215
- updatedAt: z.union([z.string(), z.date()]).describe("Updated timestamp"),
215
+ updatedAt: z.string().datetime().describe("Updated timestamp (ISO 8601)"),
216
216
  });
217
217
 
218
218
  export type SubscriptionDetail = z.infer<typeof SubscriptionDetailSchema>;
@@ -0,0 +1,257 @@
1
+ /**
2
+ * Object Storage Well-Known Binding
3
+ *
4
+ * Defines the interface for S3-compatible object storage operations.
5
+ * Any MCP that implements this binding can provide file/object management
6
+ * for buckets and objects.
7
+ *
8
+ * This binding includes:
9
+ * - LIST_OBJECTS: List objects with pagination and prefix filtering
10
+ * - GET_OBJECT_METADATA: Get object metadata (HEAD operation)
11
+ * - GET_PRESIGNED_URL: Generate presigned URL for downloading
12
+ * - PUT_PRESIGNED_URL: Generate presigned URL for uploading
13
+ * - DELETE_OBJECT: Delete a single object
14
+ * - DELETE_OBJECTS: Batch delete multiple objects
15
+ */
16
+
17
+ import { z } from "zod";
18
+ import type { Binder, ToolBinder } from "../core/binder";
19
+
20
+ // ============================================================================
21
+ // Tool Schemas
22
+ // ============================================================================
23
+
24
+ /**
25
+ * LIST_OBJECTS - List objects in the bucket with pagination support
26
+ */
27
+ const ListObjectsInputSchema = z.object({
28
+ prefix: z
29
+ .string()
30
+ .optional()
31
+ .describe("Filter objects by prefix (e.g., 'folder/' for folder contents)"),
32
+ maxKeys: z
33
+ .number()
34
+ .optional()
35
+ .default(1000)
36
+ .describe("Maximum number of keys to return (default: 1000)"),
37
+ continuationToken: z
38
+ .string()
39
+ .optional()
40
+ .describe("Token for pagination from previous response"),
41
+ delimiter: z
42
+ .string()
43
+ .optional()
44
+ .describe(
45
+ "Delimiter for grouping keys (typically '/'). When set, commonPrefixes returns folder paths.",
46
+ ),
47
+ });
48
+
49
+ const ListObjectsOutputSchema = z.object({
50
+ objects: z.array(
51
+ z.object({
52
+ key: z.string().describe("Object key/path"),
53
+ size: z.number().describe("Object size in bytes"),
54
+ lastModified: z.string().describe("Last modified timestamp"),
55
+ etag: z.string().describe("Entity tag for the object"),
56
+ }),
57
+ ),
58
+ nextContinuationToken: z
59
+ .string()
60
+ .optional()
61
+ .describe("Token for fetching next page of results"),
62
+ isTruncated: z.boolean().describe("Whether there are more results available"),
63
+ commonPrefixes: z
64
+ .array(z.string())
65
+ .optional()
66
+ .describe(
67
+ "Folder paths when delimiter is used (e.g., ['photos/2024/', 'photos/2025/'])",
68
+ ),
69
+ });
70
+
71
+ export type ListObjectsInput = z.infer<typeof ListObjectsInputSchema>;
72
+ export type ListObjectsOutput = z.infer<typeof ListObjectsOutputSchema>;
73
+
74
+ /**
75
+ * GET_OBJECT_METADATA - Get object metadata using HEAD operation
76
+ */
77
+ const GetObjectMetadataInputSchema = z.object({
78
+ key: z.string().describe("Object key/path to get metadata for"),
79
+ });
80
+
81
+ const GetObjectMetadataOutputSchema = z.object({
82
+ contentType: z.string().optional().describe("MIME type of the object"),
83
+ contentLength: z.number().describe("Size of the object in bytes"),
84
+ lastModified: z.string().describe("Last modified timestamp"),
85
+ etag: z.string().describe("Entity tag for the object"),
86
+ metadata: z
87
+ .record(z.string(), z.string())
88
+ .optional()
89
+ .describe("Custom metadata key-value pairs"),
90
+ });
91
+
92
+ export type GetObjectMetadataInput = z.infer<
93
+ typeof GetObjectMetadataInputSchema
94
+ >;
95
+ export type GetObjectMetadataOutput = z.infer<
96
+ typeof GetObjectMetadataOutputSchema
97
+ >;
98
+
99
+ /**
100
+ * GET_PRESIGNED_URL - Generate a presigned URL for downloading an object
101
+ */
102
+ const GetPresignedUrlInputSchema = z.object({
103
+ key: z.string().describe("Object key/path to generate URL for"),
104
+ expiresIn: z
105
+ .number()
106
+ .optional()
107
+ .describe(
108
+ "URL expiration time in seconds (default: from state config or 3600)",
109
+ ),
110
+ });
111
+
112
+ const GetPresignedUrlOutputSchema = z.object({
113
+ url: z.string().describe("Presigned URL for downloading the object"),
114
+ expiresIn: z.number().describe("Expiration time in seconds that was used"),
115
+ });
116
+
117
+ export type GetPresignedUrlInput = z.infer<typeof GetPresignedUrlInputSchema>;
118
+ export type GetPresignedUrlOutput = z.infer<typeof GetPresignedUrlOutputSchema>;
119
+
120
+ /**
121
+ * PUT_PRESIGNED_URL - Generate a presigned URL for uploading an object
122
+ */
123
+ const PutPresignedUrlInputSchema = z.object({
124
+ key: z.string().describe("Object key/path for the upload"),
125
+ expiresIn: z
126
+ .number()
127
+ .optional()
128
+ .describe(
129
+ "URL expiration time in seconds (default: from state config or 3600)",
130
+ ),
131
+ contentType: z
132
+ .string()
133
+ .optional()
134
+ .describe("MIME type for the object being uploaded"),
135
+ });
136
+
137
+ const PutPresignedUrlOutputSchema = z.object({
138
+ url: z.string().describe("Presigned URL for uploading the object"),
139
+ expiresIn: z.number().describe("Expiration time in seconds that was used"),
140
+ });
141
+
142
+ export type PutPresignedUrlInput = z.infer<typeof PutPresignedUrlInputSchema>;
143
+ export type PutPresignedUrlOutput = z.infer<typeof PutPresignedUrlOutputSchema>;
144
+
145
+ /**
146
+ * DELETE_OBJECT - Delete a single object
147
+ */
148
+ const DeleteObjectInputSchema = z.object({
149
+ key: z.string().describe("Object key/path to delete"),
150
+ });
151
+
152
+ const DeleteObjectOutputSchema = z.object({
153
+ success: z.boolean().describe("Whether the deletion was successful"),
154
+ key: z.string().describe("The key that was deleted"),
155
+ });
156
+
157
+ export type DeleteObjectInput = z.infer<typeof DeleteObjectInputSchema>;
158
+ export type DeleteObjectOutput = z.infer<typeof DeleteObjectOutputSchema>;
159
+
160
+ /**
161
+ * DELETE_OBJECTS - Delete multiple objects in batch
162
+ */
163
+ const DeleteObjectsInputSchema = z.object({
164
+ keys: z
165
+ .array(z.string())
166
+ .max(1000)
167
+ .describe("Array of object keys/paths to delete (max 1000)"),
168
+ });
169
+
170
+ const DeleteObjectsOutputSchema = z.object({
171
+ deleted: z.array(z.string()).describe("Array of successfully deleted keys"),
172
+ errors: z
173
+ .array(
174
+ z.object({
175
+ key: z.string(),
176
+ message: z.string(),
177
+ }),
178
+ )
179
+ .describe("Array of errors for failed deletions"),
180
+ });
181
+
182
+ export type DeleteObjectsInput = z.infer<typeof DeleteObjectsInputSchema>;
183
+ export type DeleteObjectsOutput = z.infer<typeof DeleteObjectsOutputSchema>;
184
+
185
+ // ============================================================================
186
+ // Binding Definition
187
+ // ============================================================================
188
+
189
+ /**
190
+ * Object Storage Binding
191
+ *
192
+ * Defines the interface for S3-compatible object storage operations.
193
+ * Any MCP that implements this binding can be used with the Object Storage plugin
194
+ * to provide a file browser UI.
195
+ *
196
+ * Required tools:
197
+ * - LIST_OBJECTS: List objects with prefix filtering and pagination
198
+ * - GET_OBJECT_METADATA: Get object metadata (HEAD)
199
+ * - GET_PRESIGNED_URL: Generate download URL
200
+ * - PUT_PRESIGNED_URL: Generate upload URL
201
+ * - DELETE_OBJECT: Delete single object
202
+ * - DELETE_OBJECTS: Batch delete objects
203
+ */
204
+ export const OBJECT_STORAGE_BINDING = [
205
+ {
206
+ name: "LIST_OBJECTS" as const,
207
+ inputSchema: ListObjectsInputSchema,
208
+ outputSchema: ListObjectsOutputSchema,
209
+ } satisfies ToolBinder<"LIST_OBJECTS", ListObjectsInput, ListObjectsOutput>,
210
+ {
211
+ name: "GET_OBJECT_METADATA" as const,
212
+ inputSchema: GetObjectMetadataInputSchema,
213
+ outputSchema: GetObjectMetadataOutputSchema,
214
+ } satisfies ToolBinder<
215
+ "GET_OBJECT_METADATA",
216
+ GetObjectMetadataInput,
217
+ GetObjectMetadataOutput
218
+ >,
219
+ {
220
+ name: "GET_PRESIGNED_URL" as const,
221
+ inputSchema: GetPresignedUrlInputSchema,
222
+ outputSchema: GetPresignedUrlOutputSchema,
223
+ } satisfies ToolBinder<
224
+ "GET_PRESIGNED_URL",
225
+ GetPresignedUrlInput,
226
+ GetPresignedUrlOutput
227
+ >,
228
+ {
229
+ name: "PUT_PRESIGNED_URL" as const,
230
+ inputSchema: PutPresignedUrlInputSchema,
231
+ outputSchema: PutPresignedUrlOutputSchema,
232
+ } satisfies ToolBinder<
233
+ "PUT_PRESIGNED_URL",
234
+ PutPresignedUrlInput,
235
+ PutPresignedUrlOutput
236
+ >,
237
+ {
238
+ name: "DELETE_OBJECT" as const,
239
+ inputSchema: DeleteObjectInputSchema,
240
+ outputSchema: DeleteObjectOutputSchema,
241
+ } satisfies ToolBinder<
242
+ "DELETE_OBJECT",
243
+ DeleteObjectInput,
244
+ DeleteObjectOutput
245
+ >,
246
+ {
247
+ name: "DELETE_OBJECTS" as const,
248
+ inputSchema: DeleteObjectsInputSchema,
249
+ outputSchema: DeleteObjectsOutputSchema,
250
+ } satisfies ToolBinder<
251
+ "DELETE_OBJECTS",
252
+ DeleteObjectsInput,
253
+ DeleteObjectsOutput
254
+ >,
255
+ ] as const satisfies Binder;
256
+
257
+ export type ObjectStorageBinding = typeof OBJECT_STORAGE_BINDING;
@@ -164,9 +164,11 @@ export type WorkflowExecutionStatus = z.infer<
164
164
  * Includes lock columns and retry tracking.
165
165
  */
166
166
  export const WorkflowExecutionSchema = BaseCollectionEntitySchema.extend({
167
- gateway_id: z
167
+ virtual_mcp_id: z
168
168
  .string()
169
- .describe("ID of the gateway that will be used to execute the workflow"),
169
+ .describe(
170
+ "ID of the virtual MCP (agent) that will be used to execute the workflow",
171
+ ),
170
172
  status: WorkflowExecutionStatusEnum.describe(
171
173
  "Current status of the workflow execution",
172
174
  ),