@decocms/bindings 1.1.3 → 1.2.1

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,13 @@
1
1
  {
2
2
  "name": "@decocms/bindings",
3
- "version": "1.1.3",
3
+ "version": "1.2.1",
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.2",
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",
@@ -21,6 +21,7 @@
21
21
  ".": "./src/index.ts",
22
22
  "./collections": "./src/well-known/collections.ts",
23
23
  "./llm": "./src/well-known/language-model.ts",
24
+ "./object-storage": "./src/well-known/object-storage.ts",
24
25
  "./connection": "./src/core/connection.ts",
25
26
  "./client": "./src/core/client/index.ts",
26
27
  "./mcp": "./src/well-known/mcp.ts",
@@ -34,6 +35,11 @@
34
35
  "engines": {
35
36
  "node": ">=24.0.0"
36
37
  },
38
+ "repository": {
39
+ "type": "git",
40
+ "url": "git+https://github.com/decocms/mesh.git",
41
+ "directory": "packages/bindings"
42
+ },
37
43
  "publishConfig": {
38
44
  "access": "public"
39
45
  }
@@ -47,7 +47,7 @@ export interface ServerClient {
47
47
  callTool: Client["callTool"];
48
48
  listTools: () => Promise<ListToolsResult>;
49
49
  };
50
- callStreamableTool: (
50
+ callStreamableTool?: (
51
51
  tool: string,
52
52
  args: Record<string, unknown>,
53
53
  signal?: AbortSignal,
@@ -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
- return callStreamableTool(String(toolName), args);
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
- return structuredContent;
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
- * React context provider and hook for accessing plugin context.
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
  }}
@@ -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
 
@@ -95,3 +103,10 @@ export {
95
103
  type RouteIds,
96
104
  type RouteById,
97
105
  } from "./plugin-router";
106
+
107
+ // Note: PluginContextProvider and usePluginContext have been moved to @decocms/mesh-sdk/plugins.
108
+ // Types are re-exported here for backwards compatibility.
109
+ export type {
110
+ PluginContextProviderProps,
111
+ UsePluginContextOptions,
112
+ } 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.
@@ -51,6 +52,63 @@ export interface ServerPluginContext {
51
52
  };
52
53
  }
53
54
 
55
+ /**
56
+ * Event handler context provided to plugin event handlers.
57
+ * Contains the organization ID and a publish function for emitting follow-up events.
58
+ */
59
+ export interface ServerPluginEventContext {
60
+ /** Organization ID the events belong to */
61
+ organizationId: string;
62
+ /** Connection ID of the SELF MCP for this organization */
63
+ connectionId: string;
64
+ /** Publish a follow-up event to the event bus */
65
+ publish: (
66
+ type: string,
67
+ subject: string,
68
+ data?: Record<string, unknown>,
69
+ options?: { deliverAt?: string },
70
+ ) => Promise<void>;
71
+ /** Create an MCP proxy client for calling tools on a connection */
72
+ createMCPProxy: (connectionId: string) => Promise<{
73
+ callTool: (
74
+ params: { name: string; arguments?: Record<string, unknown> },
75
+ resultSchema?: unknown,
76
+ options?: { timeout?: number },
77
+ ) => Promise<{
78
+ content?: unknown;
79
+ structuredContent?: unknown;
80
+ isError?: boolean;
81
+ }>;
82
+ close: () => Promise<void>;
83
+ }>;
84
+ }
85
+
86
+ /**
87
+ * Startup context provided to plugin onStartup hooks.
88
+ * Contains the database and a publish function for emitting recovery events.
89
+ */
90
+ export interface ServerPluginStartupContext {
91
+ /** Database instance */
92
+ db: Kysely<unknown>;
93
+ /** Publish an event to the event bus for a given organization */
94
+ publish: (
95
+ organizationId: string,
96
+ event: { type: string; subject: string; data?: Record<string, unknown> },
97
+ ) => Promise<void>;
98
+ }
99
+
100
+ /**
101
+ * Event definition for a CloudEvent received by a plugin.
102
+ */
103
+ export interface ServerPluginEvent {
104
+ id: string;
105
+ type: string;
106
+ source: string;
107
+ subject?: string;
108
+ data?: unknown;
109
+ time?: string;
110
+ }
111
+
54
112
  /**
55
113
  * Server Plugin interface.
56
114
  *
@@ -95,6 +153,35 @@ export interface ServerPlugin {
95
153
  * Called during context initialization.
96
154
  */
97
155
  createStorage?: (ctx: ServerPluginContext) => unknown;
156
+
157
+ /**
158
+ * Event handler for this plugin.
159
+ *
160
+ * When defined, the system will:
161
+ * 1. Auto-subscribe the SELF connection to the specified event types per-organization
162
+ * 2. Route matching events from the event bus to this handler
163
+ *
164
+ * Events are durable (persisted in the event bus) with at-least-once delivery.
165
+ * The handler receives batches of events and a context for publishing follow-up events.
166
+ */
167
+ onEvents?: {
168
+ /** Event type patterns this plugin handles (e.g., "workflow.execution.created") */
169
+ types: string[];
170
+ /** Handle a batch of events. Errors are logged but don't affect other plugins. */
171
+ handler: (
172
+ events: ServerPluginEvent[],
173
+ ctx: ServerPluginEventContext,
174
+ ) => Promise<void> | void;
175
+ };
176
+
177
+ /**
178
+ * Startup hook called once after the event bus is ready.
179
+ *
180
+ * Use this to recover from crashes (e.g., resume stuck workflow executions).
181
+ * Called after storage is initialized and the event bus worker has started.
182
+ * Errors are logged but don't prevent other plugins from starting.
183
+ */
184
+ onStartup?: (ctx: ServerPluginStartupContext) => Promise<void>;
98
185
  }
99
186
 
100
187
  /**
package/src/index.ts CHANGED
@@ -17,7 +17,7 @@ export {
17
17
  type ConnectionForBinding,
18
18
  } from "./core/binder";
19
19
 
20
- // Re-export plugin context types and provider
20
+ // Re-export plugin context types (not the React provider - use @decocms/bindings/plugins for that)
21
21
  export {
22
22
  type PluginContext,
23
23
  type PluginContextPartial,
@@ -27,13 +27,6 @@ export {
27
27
  type TypedToolCaller,
28
28
  } from "./core/plugin-context";
29
29
 
30
- export {
31
- PluginContextProvider,
32
- usePluginContext,
33
- type PluginContextProviderProps,
34
- type UsePluginContextOptions,
35
- } from "./core/plugin-context-provider";
36
-
37
30
  // Re-export registry binding types
38
31
  export {
39
32
  MCPRegistryServerSchema,
@@ -108,3 +101,6 @@ export {
108
101
  type DeleteObjectsInput,
109
102
  type DeleteObjectsOutput,
110
103
  } from "./well-known/object-storage";
104
+
105
+ // Re-export workflow binding types
106
+ export { WORKFLOWS_COLLECTION_BINDING } from "./well-known/workflow";
@@ -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