@waniwani/sdk 0.11.11 → 0.11.13

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.
Files changed (53) hide show
  1. package/README.md +78 -112
  2. package/dist/chat/embed.js +65 -65
  3. package/dist/chat/embed.js.map +1 -1
  4. package/dist/chat/express-js/index.d.ts +8 -1
  5. package/dist/chat/express-js/index.js.map +1 -1
  6. package/dist/chat/index.d.ts +9 -22
  7. package/dist/chat/index.js +7 -7
  8. package/dist/chat/index.js.map +1 -1
  9. package/dist/chat/next-js/index.d.ts +5 -3
  10. package/dist/chat/next-js/index.js.map +1 -1
  11. package/dist/chat/server/index.js +1 -1
  12. package/dist/chat/server/index.js.map +1 -1
  13. package/dist/chat/styles.css +1 -1
  14. package/dist/{chunk-DGSC74SV.js → chunk-DP6SAQTK.js} +1 -1
  15. package/dist/chunk-DP6SAQTK.js.map +1 -0
  16. package/dist/{chunk-5OQXAEHG.js → chunk-RZKVTH7F.js} +1 -1
  17. package/dist/chunk-RZKVTH7F.js.map +1 -0
  18. package/dist/legacy/chat/express-js/index.d.ts +407 -0
  19. package/dist/legacy/chat/express-js/index.js +10 -0
  20. package/dist/legacy/chat/express-js/index.js.map +1 -0
  21. package/dist/legacy/chat/next-js/index.d.ts +368 -0
  22. package/dist/legacy/chat/next-js/index.js +10 -0
  23. package/dist/legacy/chat/next-js/index.js.map +1 -0
  24. package/dist/legacy/index.d.ts +335 -0
  25. package/dist/legacy/index.js +2 -0
  26. package/dist/legacy/index.js.map +1 -0
  27. package/dist/legacy/mcp/react.d.ts +573 -0
  28. package/dist/legacy/mcp/react.js +68 -0
  29. package/dist/legacy/mcp/react.js.map +1 -0
  30. package/dist/mcp/index.d.ts +1153 -1117
  31. package/dist/mcp/index.js +5 -5
  32. package/dist/mcp/index.js.map +1 -1
  33. package/dist/mcp/react.d.ts +170 -74
  34. package/dist/mcp/react.js +7 -7
  35. package/dist/mcp/react.js.map +1 -1
  36. package/dist/{mcp-apps-client-PUL4H54S.js → mcp-apps-client-OFYMQOI3.js} +1 -1
  37. package/dist/mcp-apps-client-OFYMQOI3.js.map +1 -0
  38. package/dist/{openai-client-QAC3ZD5W.js → openai-client-TZIOCMXP.js} +2 -2
  39. package/dist/openai-client-TZIOCMXP.js.map +1 -0
  40. package/dist/platform-LKQFC3AJ.js +3 -0
  41. package/package.json +34 -21
  42. package/dist/chunk-5OQXAEHG.js.map +0 -1
  43. package/dist/chunk-DGSC74SV.js.map +0 -1
  44. package/dist/evals/index.d.ts +0 -156
  45. package/dist/evals/index.js +0 -2
  46. package/dist/evals/index.js.map +0 -1
  47. package/dist/evals/scorers.d.ts +0 -92
  48. package/dist/evals/scorers.js +0 -8
  49. package/dist/evals/scorers.js.map +0 -1
  50. package/dist/mcp-apps-client-PUL4H54S.js.map +0 -1
  51. package/dist/openai-client-QAC3ZD5W.js.map +0 -1
  52. package/dist/platform-GKYYQBCS.js +0 -3
  53. /package/dist/{platform-GKYYQBCS.js.map → platform-LKQFC3AJ.js.map} +0 -0
@@ -1,285 +1,237 @@
1
- import { ContentBlock, CallToolResult, ServerRequest, ServerNotification } from '@modelcontextprotocol/sdk/types.js';
2
1
  import { ZodRawShapeCompat, ShapeOutput } from '@modelcontextprotocol/sdk/server/zod-compat.js';
3
2
  export { ZodRawShapeCompat } from '@modelcontextprotocol/sdk/server/zod-compat.js';
4
3
  import { RequestHandlerExtra } from '@modelcontextprotocol/sdk/shared/protocol.js';
4
+ import { ServerRequest, ServerNotification, CallToolResult, ContentBlock } from '@modelcontextprotocol/sdk/types.js';
5
5
  import { z } from 'zod';
6
6
  import { McpServer, ToolCallback } from '@modelcontextprotocol/sdk/server/mcp.js';
7
7
  export { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
8
8
 
9
+ interface SearchResult {
10
+ source: string;
11
+ heading: string;
12
+ content: string;
13
+ score: number;
14
+ metadata?: Record<string, string>;
15
+ }
16
+ /** A file to ingest into the knowledge base */
17
+ interface KbIngestFile {
18
+ /** Filename (used as chunk source identifier) */
19
+ filename: string;
20
+ /** Markdown content of the file */
21
+ content: string;
22
+ /** Arbitrary key-value metadata attached to all chunks from this file */
23
+ metadata?: Record<string, string>;
24
+ }
25
+ /** Response from the ingest endpoint */
26
+ interface KbIngestResult {
27
+ /** Number of chunks created from the ingested files */
28
+ chunksIngested: number;
29
+ /** Number of files successfully processed */
30
+ filesProcessed: number;
31
+ }
32
+ /** Options for the search method */
33
+ interface KbSearchOptions {
34
+ /** Number of results to return (1-20, default 5) */
35
+ topK?: number;
36
+ /** Minimum similarity score threshold (0-1, default 0.3) */
37
+ minScore?: number;
38
+ /** Filter results to chunks whose metadata contains all these key-value pairs (exact match) */
39
+ metadata?: Record<string, string>;
40
+ }
41
+ /** A source entry in the knowledge base */
42
+ interface KbSource {
43
+ /** Source filename */
44
+ source: string;
45
+ /** Number of chunks from this source */
46
+ chunkCount: number;
47
+ /** ISO timestamp of when the source was first ingested */
48
+ createdAt: string;
49
+ }
50
+ /** KB client for server-side knowledge base operations */
51
+ interface KbClient {
52
+ /**
53
+ * Ingest files into the knowledge base.
54
+ *
55
+ * **Warning**: This is destructive — it deletes ALL existing chunks
56
+ * for the environment before ingesting the new files.
57
+ */
58
+ ingest(files: KbIngestFile[]): Promise<KbIngestResult>;
59
+ /** Search the knowledge base for relevant chunks */
60
+ search(query: string, options?: KbSearchOptions): Promise<SearchResult[]>;
61
+ /** List all sources in the knowledge base */
62
+ sources(): Promise<KbSource[]>;
63
+ }
64
+
65
+ type EventType = "session.started" | "tool.called" | "quote.requested" | "quote.succeeded" | "quote.failed" | "link.clicked" | "purchase.completed" | "widget_render" | "widget_click" | "widget_link_click" | "widget_error" | "widget_scroll" | "widget_form_field" | "widget_form_submit" | "user.identified";
66
+ interface ToolCalledProperties {
67
+ name?: string;
68
+ type?: "pricing" | "product_info" | "availability" | "support" | "other";
69
+ }
70
+ interface QuoteSucceededProperties {
71
+ amount?: number;
72
+ currency?: string;
73
+ }
74
+ interface LinkClickedProperties {
75
+ url?: string;
76
+ }
77
+ interface PurchaseCompletedProperties {
78
+ amount?: number;
79
+ currency?: string;
80
+ }
81
+ interface TrackingContext {
82
+ /**
83
+ * MCP request metadata passed through to the API.
84
+ *
85
+ * Location varies by MCP library:
86
+ * - `@vercel/mcp-handler`: `extra._meta`
87
+ * - `@modelcontextprotocol/sdk`: `request.params._meta`
88
+ */
89
+ meta?: Record<string, unknown>;
90
+ /** Legacy metadata field supported for backward compatibility. */
91
+ metadata?: Record<string, unknown>;
92
+ /** Optional explicit correlation fields. */
93
+ sessionId?: string;
94
+ traceId?: string;
95
+ requestId?: string;
96
+ correlationId?: string;
97
+ externalUserId?: string;
98
+ /** Optional explicit envelope fields. */
99
+ eventId?: string;
100
+ timestamp?: string | Date;
101
+ source?: string;
102
+ }
9
103
  /**
10
- * Widget platform types
104
+ * Modern tracking shape (preferred).
11
105
  */
12
- type WidgetPlatform = "openai" | "mcp-apps";
106
+ interface BaseTrackEvent extends TrackingContext {
107
+ event: EventType;
108
+ properties?: Record<string, unknown>;
109
+ }
110
+ type TrackEvent = ({
111
+ event: "session.started";
112
+ } & BaseTrackEvent) | ({
113
+ event: "tool.called";
114
+ properties?: ToolCalledProperties;
115
+ } & BaseTrackEvent) | ({
116
+ event: "quote.requested";
117
+ } & BaseTrackEvent) | ({
118
+ event: "quote.succeeded";
119
+ properties?: QuoteSucceededProperties;
120
+ } & BaseTrackEvent) | ({
121
+ event: "quote.failed";
122
+ } & BaseTrackEvent) | ({
123
+ event: "link.clicked";
124
+ properties?: LinkClickedProperties;
125
+ } & BaseTrackEvent) | ({
126
+ event: "purchase.completed";
127
+ properties?: PurchaseCompletedProperties;
128
+ } & BaseTrackEvent) | ({
129
+ event: "user.identified";
130
+ } & BaseTrackEvent);
13
131
  /**
14
- * Detects which platform the widget is running on.
15
- *
16
- * OpenAI injects a global `window.openai` object.
17
- * MCP Apps runs in a sandboxed iframe and uses postMessage.
132
+ * Legacy tracking shape supported for existing integrations.
18
133
  */
19
- declare function detectPlatform(): WidgetPlatform;
134
+ interface LegacyTrackEvent extends TrackingContext {
135
+ eventType: EventType;
136
+ properties?: Record<string, unknown>;
137
+ toolName?: string;
138
+ toolType?: ToolCalledProperties["type"];
139
+ quoteAmount?: number;
140
+ quoteCurrency?: string;
141
+ linkUrl?: string;
142
+ purchaseAmount?: number;
143
+ purchaseCurrency?: string;
144
+ }
20
145
  /**
21
- * Check if running on OpenAI platform
146
+ * Public track input accepted by `client.track()`.
22
147
  */
23
- declare function isOpenAI(): boolean;
148
+ type TrackInput = TrackEvent | LegacyTrackEvent;
149
+ interface TrackingConfig {
150
+ /** Events API V2 endpoint path. */
151
+ endpointPath?: string;
152
+ /** Periodic flush interval for buffered events. */
153
+ flushIntervalMs?: number;
154
+ /** Max events per HTTP batch send. */
155
+ maxBatchSize?: number;
156
+ /** Max in-memory buffer size before oldest items are dropped. */
157
+ maxBufferSize?: number;
158
+ /** Number of retries for retryable failures. */
159
+ maxRetries?: number;
160
+ /** Retry backoff base delay. */
161
+ retryBaseDelayMs?: number;
162
+ /** Retry backoff max delay. */
163
+ retryMaxDelayMs?: number;
164
+ /** Default shutdown timeout when none is provided. */
165
+ shutdownTimeoutMs?: number;
166
+ }
167
+ interface TrackingShutdownOptions {
168
+ timeoutMs?: number;
169
+ }
170
+ interface TrackingShutdownResult {
171
+ timedOut: boolean;
172
+ pendingEvents: number;
173
+ }
24
174
  /**
25
- * Check if running on MCP Apps platform
175
+ * Tracking module methods for WaniWaniClient.
26
176
  */
27
- declare function isMCPApps(): boolean;
28
-
29
- type ModelContextContentBlock = ContentBlock;
30
- type ModelContextUpdate = {
31
- content?: ModelContextContentBlock[];
32
- structuredContent?: Record<string, unknown>;
33
- };
177
+ interface TrackingClient {
178
+ /**
179
+ * Send a one-shot identify event for a user.
180
+ * userId can be any string: an email, an internal ID, etc.
181
+ */
182
+ identify: (userId: string, properties?: Record<string, unknown>, meta?: Record<string, unknown>) => Promise<{
183
+ eventId: string;
184
+ }>;
185
+ /**
186
+ * Track an event using modern or legacy input shape.
187
+ * Returns a deterministic event id immediately after enqueue.
188
+ */
189
+ track: (event: TrackInput) => Promise<{
190
+ eventId: string;
191
+ }>;
192
+ /**
193
+ * Flush all currently buffered events.
194
+ */
195
+ flush: () => Promise<void>;
196
+ /**
197
+ * Flush and stop the transport.
198
+ */
199
+ shutdown: (options?: TrackingShutdownOptions) => Promise<TrackingShutdownResult>;
200
+ }
34
201
 
35
202
  /**
36
- * Source: https://github.com/openai/openai-apps-sdk-examples/tree/main/src
203
+ * A request-scoped WaniWani client with meta pre-attached.
204
+ *
205
+ * Available as `context.waniwani` inside `createTool` handlers and flow nodes
206
+ * when the server is wrapped with `withWaniwani()`.
37
207
  */
38
- type OpenAIGlobals<ToolInput = UnknownObject, ToolOutput = UnknownObject, ToolResponseMetadata = UnknownObject, WidgetState = UnknownObject> = {
39
- theme: Theme;
40
- userAgent: UserAgent;
41
- locale: string;
42
- maxHeight: number;
43
- displayMode: DisplayMode;
44
- safeArea: SafeArea;
45
- toolInput: ToolInput;
46
- toolOutput: ToolOutput | null;
47
- toolResponseMetadata: ToolResponseMetadata | null;
48
- widgetState: WidgetState | null;
49
- setWidgetState: (state: WidgetState) => Promise<void>;
50
- };
51
- type API = {
52
- callTool: CallTool;
53
- sendFollowUpMessage: (args: {
54
- prompt: string;
55
- }) => Promise<void>;
56
- openExternal(payload: {
57
- href: string;
58
- }): void;
59
- requestDisplayMode: RequestDisplayMode;
60
- };
61
- type UnknownObject = Record<string, unknown>;
62
- type Theme = "light" | "dark";
63
- type SafeAreaInsets = {
64
- top: number;
65
- bottom: number;
66
- left: number;
67
- right: number;
68
- };
69
- type SafeArea = {
70
- insets: SafeAreaInsets;
71
- };
72
- type DeviceType = "mobile" | "tablet" | "desktop" | "unknown";
73
- type UserAgent = {
74
- device: {
75
- type: DeviceType;
76
- };
77
- capabilities: {
78
- hover: boolean;
79
- touch: boolean;
80
- };
81
- };
82
- /** Display mode */
83
- type DisplayMode = "pip" | "inline" | "fullscreen";
84
- type RequestDisplayMode = (args: {
85
- mode: DisplayMode;
86
- }) => Promise<{
87
- /**
88
- * The granted display mode. The host may reject the request.
89
- * For mobile, PiP is always coerced to fullscreen.
90
- */
91
- mode: DisplayMode;
92
- }>;
93
- type CallToolResponse = {
94
- result: string;
95
- };
96
- /** Calling APIs */
97
- type CallTool = (name: string, args: Record<string, unknown>) => Promise<CallToolResponse>;
98
- /** Extra events */
99
- declare const SET_GLOBALS_EVENT_TYPE = "openai:set_globals";
100
- declare class SetGlobalsEvent extends CustomEvent<{
101
- globals: Partial<OpenAIGlobals>;
102
- }> {
103
- constructor(detail: {
104
- globals: Partial<OpenAIGlobals>;
105
- });
106
- }
107
- /**
108
- * Global oai object injected by the web sandbox for communicating with chatgpt host page.
109
- */
110
- declare global {
111
- interface Window {
112
- openai: API & OpenAIGlobals;
113
- innerBaseUrl: string;
114
- }
115
- interface WindowEventMap {
116
- [SET_GLOBALS_EVENT_TYPE]: SetGlobalsEvent;
117
- }
118
- }
119
-
120
- /**
121
- * Result from calling a tool
122
- */
123
- type ToolCallResult = CallToolResult;
124
- /**
125
- * Tool result notification (what the host pushes to the widget)
126
- */
127
- type ToolResult = CallToolResult;
128
- /**
129
- * Host context - all values available from the host.
130
- */
131
- type HostContext = {
132
- theme: Theme;
133
- locale: string;
134
- displayMode: DisplayMode;
135
- maxHeight: number | null;
136
- safeArea: SafeArea | null;
137
- toolOutput: UnknownObject | null;
138
- toolResponseMetadata: UnknownObject | null;
139
- widgetState: UnknownObject | null;
140
- };
141
- /**
142
- * Unified widget client interface that works on both OpenAI and MCP Apps.
143
- *
144
- * Platform-specific behavior:
145
- * - Display mode: OpenAI-only. MCP Apps returns "inline" and requestDisplayMode is a no-op.
146
- * - Follow-up messages: Unified API, different underlying implementations.
147
- */
148
- interface UnifiedWidgetClient {
149
- /**
150
- * Connect to the host. Must be called before using other methods.
151
- * On OpenAI, this is a no-op (already connected via window.openai).
152
- * On MCP Apps, this establishes the postMessage connection.
153
- */
154
- connect(): Promise<void>;
155
- /**
156
- * Close the connection to the host and clean up resources.
157
- * On OpenAI, this is a no-op.
158
- * On MCP Apps, this closes the transport and removes event listeners.
159
- */
160
- close(): Promise<void>;
161
- /**
162
- * Get the tool output (structured content returned by the tool handler).
163
- * This is the main data source for widget rendering.
164
- */
165
- getToolOutput<T = Record<string, unknown>>(): T | null;
166
- /**
167
- * Register a callback for when tool results are received.
168
- * On OpenAI, this subscribes to toolOutput changes.
169
- * On MCP Apps, this sets app.ontoolresult.
170
- */
171
- onToolResult(callback: (result: ToolResult) => void): () => void;
172
- /**
173
- * Call another tool on the server.
174
- */
175
- callTool(name: string, args: Record<string, unknown>): Promise<ToolCallResult>;
176
- /**
177
- * Open an external URL.
178
- * On OpenAI: openai.openExternal({ href })
179
- * On MCP Apps: app.sendOpenLink(url)
180
- */
181
- openExternal(url: string): void;
182
- /**
183
- * Send a follow-up message to the AI.
184
- * On OpenAI: openai.sendFollowUpMessage({ prompt })
185
- * On MCP Apps: app.sendMessages([{ role: 'user', content: { type: 'text', text: prompt } }])
186
- */
187
- sendFollowUp(prompt: string): void | Promise<void>;
188
- /**
189
- * Update hidden model context for the next assistant turn.
190
- * On MCP Apps this uses the standard `ui/update-model-context` request.
191
- * On other hosts this may fall back to best-effort behavior.
192
- */
193
- updateModelContext(context: ModelContextUpdate): Promise<void> | void;
194
- /**
195
- * Get the current theme.
196
- */
197
- getTheme(): Theme;
198
- /**
199
- * Subscribe to theme changes.
200
- */
201
- onThemeChange(callback: (theme: Theme) => void): () => void;
202
- /**
203
- * Get the current locale.
204
- */
205
- getLocale(): string;
206
- /**
207
- * Get the current display mode.
208
- * OpenAI-only: returns "pip" | "inline" | "fullscreen"
209
- * MCP Apps: always returns "inline"
210
- */
211
- getDisplayMode(): DisplayMode;
212
- /**
213
- * Request a display mode change.
214
- * OpenAI-only: requests the mode from the host.
215
- * MCP Apps: no-op (returns current mode).
216
- */
217
- requestDisplayMode(mode: DisplayMode): Promise<DisplayMode>;
218
- /**
219
- * Subscribe to display mode changes.
220
- * OpenAI-only: subscribes to displayMode changes.
221
- * MCP Apps: callback is never called.
222
- */
223
- onDisplayModeChange(callback: (mode: DisplayMode) => void): () => void;
224
- /**
225
- * Get the safe area insets.
226
- * OpenAI-only: returns insets from window.openai.safeArea.
227
- * MCP Apps: returns null.
228
- */
229
- getSafeArea(): SafeArea | null;
230
- /**
231
- * Subscribe to safe area changes.
232
- * OpenAI-only: subscribes to safeArea changes.
233
- * MCP Apps: callback is never called.
234
- */
235
- onSafeAreaChange(callback: (safeArea: SafeArea | null) => void): () => void;
236
- /**
237
- * Get the max height constraint.
238
- * OpenAI-only: returns maxHeight from window.openai.maxHeight.
239
- * MCP Apps: returns null.
240
- */
241
- getMaxHeight(): number | null;
242
- /**
243
- * Subscribe to max height changes.
244
- * OpenAI-only: subscribes to maxHeight changes.
245
- * MCP Apps: callback is never called.
246
- */
247
- onMaxHeightChange(callback: (maxHeight: number | null) => void): () => void;
248
- /**
249
- * Get the tool response metadata.
250
- * OpenAI: returns metadata from window.openai.toolResponseMetadata.
251
- * MCP Apps: returns `_meta` from the latest `ui/notifications/tool-result`, if provided by host.
252
- */
253
- getToolResponseMetadata(): UnknownObject | null;
254
- /**
255
- * Subscribe to tool response metadata changes.
256
- * OpenAI: subscribes to toolResponseMetadata changes.
257
- * MCP Apps: fires when tool result `_meta` changes.
258
- */
259
- onToolResponseMetadataChange(callback: (metadata: UnknownObject | null) => void): () => void;
260
- /**
261
- * Get the widget state.
262
- * OpenAI-only: returns state from window.openai.widgetState.
263
- * MCP Apps: returns null.
264
- */
265
- getWidgetState(): UnknownObject | null;
266
- /**
267
- * Subscribe to widget state changes.
268
- * OpenAI-only: subscribes to widgetState changes.
269
- * MCP Apps: callback is never called.
270
- */
271
- onWidgetStateChange(callback: (state: UnknownObject | null) => void): () => void;
272
- }
273
-
274
- type WidgetCSP = {
275
- /** Domains permitted for fetch/XHR network requests */
276
- connect_domains?: string[];
277
- /** Domains for static assets (images, fonts, scripts, styles) */
278
- resource_domains?: string[];
279
- /** Origins allowed for iframe embeds (triggers stricter app review) */
280
- frame_domains?: string[];
281
- /** Origins that can receive openExternal redirects without safe-link modal */
282
- redirect_domains?: string[];
208
+ interface ScopedWaniWaniClient {
209
+ /** Track an event — request meta is automatically merged. */
210
+ track(event: TrackInput): Promise<{
211
+ eventId: string;
212
+ }>;
213
+ /** Identify a user — request meta is automatically merged. */
214
+ identify(userId: string, properties?: Record<string, unknown>): Promise<{
215
+ eventId: string;
216
+ }>;
217
+ /** Knowledge base client (no meta needed). */
218
+ readonly kb: KbClient;
219
+ /** @internal Resolved API config from withWaniwani(). */
220
+ readonly _config?: {
221
+ apiUrl?: string;
222
+ apiKey?: string;
223
+ };
224
+ }
225
+
226
+ type WidgetCSP = {
227
+ /** Domains permitted for fetch/XHR network requests */
228
+ connect_domains?: string[];
229
+ /** Domains for static assets (images, fonts, scripts, styles) */
230
+ resource_domains?: string[];
231
+ /** Origins allowed for iframe embeds (triggers stricter app review) */
232
+ frame_domains?: string[];
233
+ /** Origins that can receive openExternal redirects without safe-link modal */
234
+ redirect_domains?: string[];
283
235
  };
284
236
  type ResourceConfig = {
285
237
  /** Unique identifier for the resource */
@@ -314,809 +266,1024 @@ type RegisteredResource = {
314
266
  register: (server: McpServer) => Promise<void>;
315
267
  };
316
268
 
317
- interface SearchResult {
318
- source: string;
319
- heading: string;
320
- content: string;
321
- score: number;
322
- metadata?: Record<string, string>;
323
- }
324
- /** A file to ingest into the knowledge base */
325
- interface KbIngestFile {
326
- /** Filename (used as chunk source identifier) */
327
- filename: string;
328
- /** Markdown content of the file */
329
- content: string;
330
- /** Arbitrary key-value metadata attached to all chunks from this file */
331
- metadata?: Record<string, string>;
332
- }
333
- /** Response from the ingest endpoint */
334
- interface KbIngestResult {
335
- /** Number of chunks created from the ingested files */
336
- chunksIngested: number;
337
- /** Number of files successfully processed */
338
- filesProcessed: number;
339
- }
340
- /** Options for the search method */
341
- interface KbSearchOptions {
342
- /** Number of results to return (1-20, default 5) */
343
- topK?: number;
344
- /** Minimum similarity score threshold (0-1, default 0.3) */
345
- minScore?: number;
346
- /** Filter results to chunks whose metadata contains all these key-value pairs (exact match) */
347
- metadata?: Record<string, string>;
348
- }
349
- /** A source entry in the knowledge base */
350
- interface KbSource {
351
- /** Source filename */
352
- source: string;
353
- /** Number of chunks from this source */
354
- chunkCount: number;
355
- /** ISO timestamp of when the source was first ingested */
356
- createdAt: string;
269
+ type ToolHandlerContext = {
270
+ /** Raw MCP request extra data (includes _meta for session extraction) */
271
+ extra?: {
272
+ _meta?: Record<string, unknown>;
273
+ };
274
+ /** Session-scoped WaniWani client — available when the server is wrapped with withWaniwani() */
275
+ waniwani?: ScopedWaniWaniClient;
276
+ };
277
+ type ToolConfig<TInput extends ZodRawShapeCompat> = {
278
+ /** The resource (HTML template) this tool renders. When present, tool returns structuredContent + widget _meta. */
279
+ resource?: RegisteredResource;
280
+ /** Tool identifier. Defaults to resource.id when resource is present, required otherwise. */
281
+ id?: string;
282
+ /** Display title. Defaults to resource.title when resource is present, required otherwise. */
283
+ title?: string;
284
+ /** Action-oriented description for the tool (tells the model WHEN to use it) */
285
+ description: string;
286
+ /** UI component description (describes WHAT the widget displays). Falls back to description. Only relevant when resource is present. */
287
+ widgetDescription?: string;
288
+ /** Input schema using zod */
289
+ inputSchema: TInput;
290
+ /** Optional loading message (defaults to "Loading..."). Only relevant when resource is present. */
291
+ invoking?: string;
292
+ /** Optional loaded message (defaults to "Loaded"). Only relevant when resource is present. */
293
+ invoked?: string;
294
+ /**
295
+ * When a widget calls this tool via `tools/call`, should the host auto-inject
296
+ * the tool's text result into the next follow-up message if the widget does
297
+ * not send one itself. Defaults to true. Set false for helper tools whose
298
+ * result is consumed programmatically by the widget.
299
+ */
300
+ autoInjectResultText?: boolean;
301
+ /** Annotations describe the tool's potential impact. */
302
+ annotations?: {
303
+ readOnlyHint?: boolean;
304
+ idempotentHint?: boolean;
305
+ openWorldHint?: boolean;
306
+ destructiveHint?: boolean;
307
+ };
308
+ };
309
+ type ToolHandler<TInput extends ZodRawShapeCompat> = (input: ShapeOutput<TInput>, context: ToolHandlerContext) => Promise<{
310
+ /** Text content to return */
311
+ text: string;
312
+ /** Structured data returned as MCP `structuredContent`. */
313
+ data?: Record<string, unknown>;
314
+ }>;
315
+ type ToolToolCallback<TInput extends ZodRawShapeCompat> = ToolCallback<TInput>;
316
+ type RegisteredTool = {
317
+ id: string;
318
+ title: string;
319
+ description: string;
320
+ /** Register the tool on the server */
321
+ register: (server: McpServer) => Promise<void>;
322
+ };
323
+
324
+ /**
325
+ * Server-side flow state store.
326
+ *
327
+ * Flow state is stored via the WaniWani API, keyed by session ID.
328
+ * Config comes from env vars (WANIWANI_API_KEY, WANIWANI_API_URL).
329
+ */
330
+
331
+ interface FlowStore {
332
+ get(key: string): Promise<FlowTokenContent | null>;
333
+ set(key: string, value: FlowTokenContent): Promise<void>;
334
+ delete(key: string): Promise<void>;
357
335
  }
358
- /** KB client for server-side knowledge base operations */
359
- interface KbClient {
336
+
337
+ declare const START: "__start__";
338
+ declare const END: "__end__";
339
+ declare const INTERRUPT: unique symbol;
340
+ declare const WIDGET: unique symbol;
341
+ /** A single question within an interrupt step */
342
+ type InterruptQuestion = {
343
+ /** Question to ask the user */
344
+ question: string;
345
+ /** State key where the answer will be stored */
346
+ field: string;
347
+ /** Optional suggestions to present as options */
348
+ suggestions?: string[];
349
+ /** Hidden context/instructions for this specific question (not shown to user directly) */
350
+ context?: string;
351
+ /** Validation function — runs after the user answers, before advancing to the next node */
352
+ validate?: (value: unknown) => MaybePromise<Record<string, unknown> | void>;
353
+ };
354
+ /**
355
+ * Interrupt signal — pauses the flow and asks the user one or more questions.
356
+ * Single-question and multi-question interrupts use the same type.
357
+ */
358
+ type InterruptSignal = {
359
+ readonly __type: typeof INTERRUPT;
360
+ /** Questions to ask — ask all in one conversational message */
361
+ questions: InterruptQuestion[];
362
+ /** Overall hidden context/instructions for the assistant (not shown to user directly) */
363
+ context?: string;
364
+ };
365
+ type WidgetSignal = {
366
+ readonly __type: typeof WIDGET;
367
+ /** The id of the display tool to delegate rendering to */
368
+ tool: string;
369
+ /** Data to pass to the display tool */
370
+ data: Record<string, unknown>;
371
+ /** Description of what the widget does (for the AI's context) */
372
+ description?: string;
360
373
  /**
361
- * Ingest files into the knowledge base.
374
+ * Whether the user is expected to interact with the widget before the flow continues.
375
+ * Defaults to true. Set to false for informational widgets that should render and then
376
+ * immediately advance to the next flow step.
377
+ */
378
+ interactive?: boolean;
379
+ /**
380
+ * State key this widget fills — enables auto-skip when the field is already in state.
381
+ * Pass this so the engine can skip the widget step when the answer is already known.
382
+ */
383
+ field?: string;
384
+ };
385
+ type MaybePromise<T> = T | Promise<T>;
386
+ /** Deep partial — allows partial updates at any nesting level (for z.object state fields). */
387
+ type DeepPartial<T> = {
388
+ [K in keyof T]?: T[K] extends Record<string, unknown> ? DeepPartial<T[K]> : T[K];
389
+ };
390
+ /**
391
+ * Extract known (non-index-signature) string keys from a type.
392
+ * Zod v4's z.object() adds `[key: string]: unknown` to inferred types,
393
+ * so we filter those out to get only the declared field names.
394
+ */
395
+ type KnownStringKeys<T> = Extract<keyof {
396
+ [K in keyof T as string extends K ? never : number extends K ? never : K]: T[K];
397
+ }, string>;
398
+ /**
399
+ * Union of all valid field paths for a state type.
400
+ * - Flat fields produce their key: `"email"`
401
+ * - `z.object()` fields produce dot-paths to sub-fields: `"driver.name"`, `"driver.license"`
402
+ * - Arrays and general records (`z.record()`) are treated as flat fields.
403
+ * - Only 1 level of nesting is supported.
404
+ */
405
+ type FieldPaths<TState> = {
406
+ [K in Extract<keyof TState, string>]: NonNullable<TState[K]> extends unknown[] ? K : NonNullable<TState[K]> extends Record<string, unknown> ? KnownStringKeys<NonNullable<TState[K]>> extends never ? K : K | `${K}.${KnownStringKeys<NonNullable<TState[K]>>}` : K;
407
+ }[Extract<keyof TState, string>];
408
+ /** Resolve a dot-path to the value type at that path in TState. */
409
+ type ResolveFieldType<TState, P extends string> = P extends `${infer Parent}.${infer Child}` ? Parent extends keyof TState ? Child extends keyof NonNullable<TState[Parent]> ? NonNullable<TState[Parent]>[Child] : never : never : P extends keyof TState ? TState[P] : never;
410
+ /**
411
+ * Typed interrupt function — available on the node context.
412
+ *
413
+ * First argument: an object where each key is a field path and each value
414
+ * describes the question for that field. Use dot-paths for nested state fields.
415
+ * `validate` receives the field's value typed from the Zod schema.
416
+ *
417
+ * Second argument (optional): config with overall hidden AI instructions.
418
+ *
419
+ * @example
420
+ * ```ts
421
+ * // Flat field
422
+ * interrupt({ breed: { question: "What breed is your pet?" } })
423
+ *
424
+ * // Nested field (z.object in state)
425
+ * interrupt({ "driver.name": { question: "Driver's name?" } })
426
+ *
427
+ * // Multiple questions with context
428
+ * interrupt(
429
+ * {
430
+ * "driver.name": { question: "Name?" },
431
+ * "driver.license": { question: "License?" },
432
+ * },
433
+ * { context: "Ask both questions naturally." },
434
+ * )
435
+ * ```
436
+ */
437
+ type TypedInterrupt<TState> = (fields: {
438
+ [P in FieldPaths<TState>]?: {
439
+ question: string;
440
+ validate?: (value: ResolveFieldType<TState, P>) => MaybePromise<DeepPartial<TState> | void>;
441
+ suggestions?: string[];
442
+ context?: string;
443
+ };
444
+ }, config?: {
445
+ /** Overall hidden context/instructions for the assistant (not shown to user directly) */
446
+ context?: string;
447
+ }) => InterruptSignal;
448
+ /**
449
+ * Typed showWidget function — available on the node context.
450
+ * The `field` parameter accepts field paths (flat or dot-path for nested state).
451
+ */
452
+ type TypedShowWidget<TState> = (tool: RegisteredTool | string, config: {
453
+ data: Record<string, unknown>;
454
+ description?: string;
455
+ interactive?: boolean;
456
+ field?: FieldPaths<TState>;
457
+ }) => WidgetSignal;
458
+ /**
459
+ * Context object passed to node handlers.
460
+ * Provides state, metadata, and typed helper functions for creating signals.
461
+ */
462
+ type NodeContext<TState> = {
463
+ /** Current flow state (partial — fields are filled as the flow progresses) */
464
+ state: Partial<TState>;
465
+ /** Request metadata from the MCP call */
466
+ meta?: Record<string, unknown>;
467
+ /** Create an interrupt signal — pause and ask the user questions */
468
+ interrupt: TypedInterrupt<TState>;
469
+ /** Create a widget signal — pause and show a UI widget */
470
+ showWidget: TypedShowWidget<TState>;
471
+ /** Session-scoped WaniWani client — available when the server is wrapped with withWaniwani() */
472
+ waniwani?: ScopedWaniWaniClient;
473
+ };
474
+ /**
475
+ * Node handler — receives a context object and returns a signal or state updates.
476
+ * The return value determines behavior:
477
+ * - `Partial<TState>` → action node (state merged, auto-advance)
478
+ * - `InterruptSignal` → interrupt (pause, ask user one or more questions)
479
+ * - `WidgetSignal` → widget step (pause, show widget)
480
+ */
481
+ type NodeHandler<TState> = (ctx: NodeContext<TState>) => MaybePromise<DeepPartial<TState> | InterruptSignal | WidgetSignal>;
482
+ /**
483
+ * Condition function for conditional edges.
484
+ * Receives current state, returns the name of the next node.
485
+ */
486
+ type ConditionFn<TState> = (state: Partial<TState>) => string | Promise<string>;
487
+ type NodeOptions = {
488
+ /** Human-readable label for this node (used in funnel visualization and Graphs). */
489
+ label: string;
490
+ /** When true, this node is excluded from funnel analytics. */
491
+ hideFromFunnel?: boolean;
492
+ };
493
+ /**
494
+ * Config for the object form of `.addNode({ id, run, label?, hideFromFunnel? })`.
495
+ *
496
+ * Preferred over the positional form `.addNode(id, run, options?)` — metadata
497
+ * sits at the top where the eye lands, and the handler is a named field.
498
+ */
499
+ type AddNodeConfig<TState, TName extends string = string> = {
500
+ /** Unique node id within the flow. */
501
+ id: TName;
502
+ /** Node handler — receives ctx, returns state updates or a signal. */
503
+ run: NodeHandler<TState>;
504
+ /** Human-readable label for funnel visualization and graphs. Defaults to `id`. */
505
+ label?: string;
506
+ /** When true, this node is excluded from funnel analytics. */
507
+ hideFromFunnel?: boolean;
508
+ };
509
+ type FlowGraphNode = {
510
+ id: string;
511
+ type: "widget" | "interrupt" | "action";
512
+ label: string;
513
+ hideFromFunnel?: boolean;
514
+ };
515
+ type FlowGraphEdge = {
516
+ from: string;
517
+ to: string;
518
+ type: "direct";
519
+ } | {
520
+ from: string;
521
+ to: string[];
522
+ type: "conditional";
523
+ };
524
+ type FlowGraph = {
525
+ flowId: string;
526
+ title: string;
527
+ nodes: FlowGraphNode[];
528
+ edges: FlowGraphEdge[];
529
+ };
530
+ type FlowConfig = {
531
+ /** Unique identifier for the flow (becomes the MCP tool name) */
532
+ id: string;
533
+ /** Display title */
534
+ title: string;
535
+ /** Description for the AI (explains when to use this flow) */
536
+ description: string;
537
+ /**
538
+ * Define the flow's state — each field the flow collects.
539
+ * Keys are the field names used in `interrupt({ field })`,
540
+ * values are Zod schemas with `.describe()`.
362
541
  *
363
- * **Warning**: This is destructive it deletes ALL existing chunks
364
- * for the environment before ingesting the new files.
542
+ * The state definition serves two purposes:
543
+ * 1. Type inference `TState` is automatically derived, no explicit generic needed
544
+ * 2. AI protocol — field names, types, and descriptions are included in the tool
545
+ * description so the AI can pre-fill answers via `_meta.flow.state`
546
+ *
547
+ * @example
548
+ * ```ts
549
+ * state: {
550
+ * country: z.string().describe("Country the business is based in"),
551
+ * status: z.enum(["registered", "unregistered"]).describe("Business registration status"),
552
+ * }
553
+ * ```
365
554
  */
366
- ingest(files: KbIngestFile[]): Promise<KbIngestResult>;
367
- /** Search the knowledge base for relevant chunks */
368
- search(query: string, options?: KbSearchOptions): Promise<SearchResult[]>;
369
- /** List all sources in the knowledge base */
370
- sources(): Promise<KbSource[]>;
371
- }
555
+ state: Record<string, z.ZodType>;
556
+ /**
557
+ * If true, instruct the agent to omit PII (names, emails, phones, addresses,
558
+ * IDs, ages, birthdates) from the `intent` and `context` fields. Only adds a
559
+ * guidance line to the tool description — does not redact server-side.
560
+ */
561
+ omitIntentPII?: boolean;
562
+ /** Optional tool annotations */
563
+ annotations?: {
564
+ readOnlyHint?: boolean;
565
+ idempotentHint?: boolean;
566
+ openWorldHint?: boolean;
567
+ destructiveHint?: boolean;
568
+ };
569
+ };
570
+ /**
571
+ * Infer the runtime state type from a flow's state schema definition.
572
+ *
573
+ * @example
574
+ * ```ts
575
+ * const config = {
576
+ * state: {
577
+ * country: z.enum(["FR", "DE"]),
578
+ * status: z.enum(["registered", "unregistered"]),
579
+ * }
580
+ * };
581
+ * type MyState = InferFlowState<typeof config.state>;
582
+ * // { country: "FR" | "DE"; status: "registered" | "unregistered" }
583
+ * ```
584
+ */
585
+ type InferFlowState<T extends Record<string, z.ZodType>> = {
586
+ [K in keyof T]: z.infer<T[K]>;
587
+ };
588
+ /**
589
+ * Flow tool handler — uses shared MCP types (`RequestHandlerExtra`, `CallToolResult`)
590
+ * so it's assignable to both MCP SDK's `ToolCallback` and Skybridge's `ToolHandler`.
591
+ */
592
+ type FlowToolHandler = (args: Record<string, unknown>, extra: RequestHandlerExtra<ServerRequest, ServerNotification>) => CallToolResult | Promise<CallToolResult>;
593
+ /**
594
+ * A compiled flow — can be registered on an McpServer.
595
+ *
596
+ * Exposes MCP-compatible `name`, `config`, and `handler` so it can be
597
+ * registered directly: `server.registerTool(flow.name, flow.config, flow.handler)`
598
+ */
599
+ type RegisteredFlow = {
600
+ /** Tool name — pass to `server.registerTool(flow.name, flow.config, flow.handler)`. */
601
+ name: string;
602
+ /** Tool config object — pass to `server.registerTool(flow.name, flow.config, flow.handler)`. */
603
+ config: {
604
+ title: string;
605
+ description: string;
606
+ inputSchema: ZodRawShapeCompat;
607
+ annotations?: {
608
+ readOnlyHint?: boolean;
609
+ idempotentHint?: boolean;
610
+ openWorldHint?: boolean;
611
+ destructiveHint?: boolean;
612
+ };
613
+ };
614
+ /** Tool callback — pass to `server.registerTool(flow.name, flow.config, flow.handler)`. */
615
+ handler: FlowToolHandler;
616
+ /** Register this flow on an MCP server. Shorthand for `server.registerTool(flow.name, flow.config, flow.handler)`. */
617
+ register: (server: McpServer) => Promise<void>;
618
+ /** Returns a Mermaid `flowchart TD` diagram of the flow graph. */
619
+ graph: () => string;
620
+ flowGraph: FlowGraph;
621
+ };
622
+ type FlowTokenContent = {
623
+ step?: string;
624
+ state: Record<string, unknown>;
625
+ field?: string;
626
+ widgetId?: string;
627
+ };
628
+ type InterruptQuestionData = {
629
+ question: string;
630
+ field: string;
631
+ suggestions?: string[];
632
+ context?: string;
633
+ };
634
+ type FlowInterruptContent = {
635
+ status: "interrupt";
636
+ /** Single-question shorthand */
637
+ question?: string;
638
+ field?: string;
639
+ suggestions?: string[];
640
+ /** Multi-question */
641
+ questions?: InterruptQuestionData[];
642
+ context?: string;
643
+ };
644
+ type FlowWidgetContent = {
645
+ status: "widget";
646
+ /** Display tool to call */
647
+ tool: string;
648
+ /** Data to pass to the display tool */
649
+ data: Record<string, unknown>;
650
+ description?: string;
651
+ /** Whether the widget expects user interaction before continuing */
652
+ interactive?: boolean;
653
+ };
654
+ type FlowCompleteContent = {
655
+ status: "complete";
656
+ };
657
+ type FlowErrorContent = {
658
+ status: "error";
659
+ error: string;
660
+ };
372
661
 
373
- type EventType = "session.started" | "tool.called" | "quote.requested" | "quote.succeeded" | "quote.failed" | "link.clicked" | "purchase.completed" | "widget_render" | "widget_click" | "widget_link_click" | "widget_error" | "widget_scroll" | "widget_form_field" | "widget_form_submit" | "user.identified";
374
- interface ToolCalledProperties {
375
- name?: string;
376
- type?: "pricing" | "product_info" | "availability" | "support" | "other";
377
- }
378
- interface QuoteSucceededProperties {
379
- amount?: number;
380
- currency?: string;
381
- }
382
- interface LinkClickedProperties {
383
- url?: string;
384
- }
385
- interface PurchaseCompletedProperties {
386
- amount?: number;
387
- currency?: string;
388
- }
389
- interface TrackingContext {
662
+ /**
663
+ * A LangGraph-inspired state graph builder for MCP tools.
664
+ *
665
+ * @example
666
+ * ```ts
667
+ * const flow = new StateGraph<MyState>({
668
+ * id: "onboarding",
669
+ * title: "User Onboarding",
670
+ * description: "Guides users through onboarding",
671
+ * })
672
+ * .addNode({
673
+ * id: "ask_name",
674
+ * run: ({ interrupt }) => interrupt({ question: "What's your name?", field: "name" }),
675
+ * })
676
+ * .addNode({
677
+ * id: "greet",
678
+ * run: ({ state }) => ({ greeting: `Hello ${state.name}!` }),
679
+ * })
680
+ * .addEdge(START, "ask_name")
681
+ * .addEdge("ask_name", "greet")
682
+ * .addEdge("greet", END)
683
+ * .compile();
684
+ * ```
685
+ */
686
+ declare class StateGraph<TState extends Record<string, unknown>, TNodes extends string = never> {
687
+ private nodes;
688
+ private edges;
689
+ private nodeOptions;
690
+ private config;
691
+ constructor(config: FlowConfig);
390
692
  /**
391
- * MCP request metadata passed through to the API.
693
+ * Add a node with a handler.
392
694
  *
393
- * Location varies by MCP library:
394
- * - `@vercel/mcp-handler`: `extra._meta`
395
- * - `@modelcontextprotocol/sdk`: `request.params._meta`
695
+ * The handler receives a context object with `state`, `meta`, `interrupt`, and `showWidget`.
696
+ *
697
+ * @example
698
+ * ```ts
699
+ * .addNode({
700
+ * id: "ask_name",
701
+ * label: "Ask for name",
702
+ * run: ({ interrupt }) => interrupt({ question: "What's your name?", field: "name" }),
703
+ * })
704
+ * ```
396
705
  */
397
- meta?: Record<string, unknown>;
398
- /** Legacy metadata field supported for backward compatibility. */
399
- metadata?: Record<string, unknown>;
400
- /** Optional explicit correlation fields. */
401
- sessionId?: string;
402
- traceId?: string;
403
- requestId?: string;
404
- correlationId?: string;
405
- externalUserId?: string;
406
- /** Optional explicit envelope fields. */
407
- eventId?: string;
408
- timestamp?: string | Date;
409
- source?: string;
410
- }
411
- /**
412
- * Modern tracking shape (preferred).
413
- */
414
- interface BaseTrackEvent extends TrackingContext {
415
- event: EventType;
416
- properties?: Record<string, unknown>;
417
- }
418
- type TrackEvent = ({
419
- event: "session.started";
420
- } & BaseTrackEvent) | ({
421
- event: "tool.called";
422
- properties?: ToolCalledProperties;
423
- } & BaseTrackEvent) | ({
424
- event: "quote.requested";
425
- } & BaseTrackEvent) | ({
426
- event: "quote.succeeded";
427
- properties?: QuoteSucceededProperties;
428
- } & BaseTrackEvent) | ({
429
- event: "quote.failed";
430
- } & BaseTrackEvent) | ({
431
- event: "link.clicked";
432
- properties?: LinkClickedProperties;
433
- } & BaseTrackEvent) | ({
434
- event: "purchase.completed";
435
- properties?: PurchaseCompletedProperties;
436
- } & BaseTrackEvent) | ({
437
- event: "user.identified";
438
- } & BaseTrackEvent);
439
- /**
440
- * Legacy tracking shape supported for existing integrations.
441
- */
442
- interface LegacyTrackEvent extends TrackingContext {
443
- eventType: EventType;
444
- properties?: Record<string, unknown>;
445
- toolName?: string;
446
- toolType?: ToolCalledProperties["type"];
447
- quoteAmount?: number;
448
- quoteCurrency?: string;
449
- linkUrl?: string;
450
- purchaseAmount?: number;
451
- purchaseCurrency?: string;
452
- }
453
- /**
454
- * Public track input accepted by `client.track()`.
455
- */
456
- type TrackInput = TrackEvent | LegacyTrackEvent;
457
- interface TrackingConfig {
458
- /** Events API V2 endpoint path. */
459
- endpointPath?: string;
460
- /** Periodic flush interval for buffered events. */
461
- flushIntervalMs?: number;
462
- /** Max events per HTTP batch send. */
463
- maxBatchSize?: number;
464
- /** Max in-memory buffer size before oldest items are dropped. */
465
- maxBufferSize?: number;
466
- /** Number of retries for retryable failures. */
467
- maxRetries?: number;
468
- /** Retry backoff base delay. */
469
- retryBaseDelayMs?: number;
470
- /** Retry backoff max delay. */
471
- retryMaxDelayMs?: number;
472
- /** Default shutdown timeout when none is provided. */
473
- shutdownTimeoutMs?: number;
474
- }
475
- interface TrackingShutdownOptions {
476
- timeoutMs?: number;
477
- }
478
- interface TrackingShutdownResult {
479
- timedOut: boolean;
480
- pendingEvents: number;
481
- }
482
- /**
483
- * Tracking module methods for WaniWaniClient.
484
- */
485
- interface TrackingClient {
706
+ addNode<TName extends string>(config: AddNodeConfig<TState, TName>): StateGraph<TState, TNodes | TName>;
486
707
  /**
487
- * Send a one-shot identify event for a user.
488
- * userId can be any string: an email, an internal ID, etc.
708
+ * @deprecated Use the object form: `.addNode({ id, run, label? })`.
709
+ * The positional form will be removed in v0.13.0
489
710
  */
490
- identify: (userId: string, properties?: Record<string, unknown>, meta?: Record<string, unknown>) => Promise<{
491
- eventId: string;
492
- }>;
711
+ addNode<TName extends string>(name: TName, handler: NodeHandler<TState>, options?: NodeOptions): StateGraph<TState, TNodes | TName>;
493
712
  /**
494
- * Track an event using modern or legacy input shape.
495
- * Returns a deterministic event id immediately after enqueue.
713
+ * Add a direct edge between two nodes.
714
+ *
715
+ * Use `START` as `from` to set the entry point.
716
+ * Use `END` as `to` to mark a terminal node.
496
717
  */
497
- track: (event: TrackInput) => Promise<{
498
- eventId: string;
499
- }>;
718
+ addEdge(from: typeof START | TNodes, to: TNodes | typeof END): this;
500
719
  /**
501
- * Flush all currently buffered events.
720
+ * Add a conditional edge from a node.
721
+ *
722
+ * The condition function receives current state and returns the name of the next node.
502
723
  */
503
- flush: () => Promise<void>;
724
+ addConditionalEdge(from: TNodes, condition: (state: Partial<TState>) => TNodes | typeof END | Promise<TNodes | typeof END>): this;
504
725
  /**
505
- * Flush and stop the transport.
726
+ * Generate a Mermaid `flowchart TD` diagram of the graph.
727
+ *
728
+ * Direct edges use solid arrows. Conditional edges use a dashed arrow
729
+ * to a placeholder since branch targets are determined at runtime.
730
+ *
731
+ * @example
732
+ * ```ts
733
+ * console.log(graph.graph());
734
+ * // flowchart TD
735
+ * // __start__((Start))
736
+ * // ask_name[ask_name]
737
+ * // greet[greet]
738
+ * // __end__((End))
739
+ * // __start__ --> ask_name
740
+ * // ask_name --> greet
741
+ * // greet --> __end__
742
+ * ```
506
743
  */
507
- shutdown: (options?: TrackingShutdownOptions) => Promise<TrackingShutdownResult>;
744
+ graph(): string;
745
+ /**
746
+ * Compile the graph into a RegisteredFlow that can be registered on an McpServer.
747
+ *
748
+ * Validates the graph structure and returns a registration-compatible object.
749
+ */
750
+ compile(options?: {
751
+ store?: FlowStore;
752
+ }): RegisteredFlow;
753
+ private validate;
508
754
  }
509
755
 
510
756
  /**
511
- * A request-scoped WaniWani client with meta pre-attached.
757
+ * Create a new flow graph convenience factory for `new StateGraph()`.
512
758
  *
513
- * Available as `context.waniwani` inside `createTool` handlers and flow nodes
514
- * when the server is wrapped with `withWaniwani()`.
759
+ * The state type is automatically inferred from the `state` definition
760
+ * no explicit generic parameter needed.
761
+ *
762
+ * @example
763
+ * ```ts
764
+ * import { createFlow, interrupt, START, END } from "@waniwani/sdk/mcp";
765
+ * import { z } from "zod";
766
+ *
767
+ * const flow = createFlow({
768
+ * id: "onboarding",
769
+ * title: "User Onboarding",
770
+ * description: "Guides users through onboarding. Use when a user wants to get started.",
771
+ * state: {
772
+ * name: z.string().describe("The user's name"),
773
+ * email: z.string().describe("The user's email address"),
774
+ * },
775
+ * })
776
+ * .addNode({
777
+ * id: "ask_name",
778
+ * run: () => interrupt({ question: "What's your name?", field: "name" }),
779
+ * })
780
+ * .addNode({
781
+ * id: "ask_email",
782
+ * run: () => interrupt({ question: "What's your email?", field: "email" }),
783
+ * })
784
+ * .addEdge(START, "ask_name")
785
+ * .addEdge("ask_name", "ask_email")
786
+ * .addEdge("ask_email", END)
787
+ * .compile();
788
+ * ```
515
789
  */
516
- interface ScopedWaniWaniClient {
517
- /** Track an event — request meta is automatically merged. */
518
- track(event: TrackInput): Promise<{
519
- eventId: string;
520
- }>;
521
- /** Identify a user — request meta is automatically merged. */
522
- identify(userId: string, properties?: Record<string, unknown>): Promise<{
523
- eventId: string;
524
- }>;
525
- /** Knowledge base client (no meta needed). */
526
- readonly kb: KbClient;
527
- /** @internal Resolved API config from withWaniwani(). */
528
- readonly _config?: {
529
- apiUrl?: string;
530
- apiKey?: string;
531
- };
532
- }
790
+ declare function createFlow<const TSchema extends Record<string, z.ZodType>>(config: Omit<FlowConfig, "state"> & {
791
+ state: TSchema;
792
+ }): StateGraph<InferFlowState<TSchema>>;
533
793
 
534
- type ToolHandlerContext = {
535
- /** Raw MCP request extra data (includes _meta for session extraction) */
536
- extra?: {
537
- _meta?: Record<string, unknown>;
538
- };
539
- /** Session-scoped WaniWani client available when the server is wrapped with withWaniwani() */
540
- waniwani?: ScopedWaniWaniClient;
541
- };
542
- type ToolConfig<TInput extends ZodRawShapeCompat> = {
543
- /** The resource (HTML template) this tool renders. When present, tool returns structuredContent + widget _meta. */
544
- resource?: RegisteredResource;
545
- /** Tool identifier. Defaults to resource.id when resource is present, required otherwise. */
546
- id?: string;
547
- /** Display title. Defaults to resource.title when resource is present, required otherwise. */
548
- title?: string;
549
- /** Action-oriented description for the tool (tells the model WHEN to use it) */
550
- description: string;
551
- /** UI component description (describes WHAT the widget displays). Falls back to description. Only relevant when resource is present. */
552
- widgetDescription?: string;
553
- /** Input schema using zod */
554
- inputSchema: TInput;
555
- /** Optional loading message (defaults to "Loading..."). Only relevant when resource is present. */
556
- invoking?: string;
557
- /** Optional loaded message (defaults to "Loaded"). Only relevant when resource is present. */
558
- invoked?: string;
559
- /**
560
- * When a widget calls this tool via `tools/call`, should the host auto-inject
561
- * the tool's text result into the next follow-up message if the widget does
562
- * not send one itself. Defaults to true. Set false for helper tools whose
563
- * result is consumed programmatically by the widget.
564
- */
565
- autoInjectResultText?: boolean;
566
- /** Annotations describe the tool's potential impact. */
567
- annotations?: {
568
- readOnlyHint?: boolean;
569
- idempotentHint?: boolean;
570
- openWorldHint?: boolean;
571
- destructiveHint?: boolean;
572
- };
794
+ /**
795
+ * Mark a Zod schema as PII its value will be replaced with `"REDACTED"` in
796
+ * any `tool.called` event payload sent to the WaniWani API. The handler still
797
+ * receives the original value; only the tracked copy is scrubbed.
798
+ *
799
+ * Apply at the end of the schema chain so the marker is attached to the final
800
+ * schema:
801
+ *
802
+ * ```ts
803
+ * ages: redacted(z.string().describe("Comma-separated ages")),
804
+ * zipcode: redacted(z.string().describe("Spanish postal code")),
805
+ * ```
806
+ *
807
+ * Uses Zod v4's `.meta()` registry, so the marker is preserved across schema
808
+ * clones (`.optional()`, `.default()`, etc.).
809
+ */
810
+ declare function redacted<T extends z.ZodType>(schema: T): T;
811
+
812
+ type WithDecodedState = {
813
+ decodedState: FlowTokenContent | null;
573
814
  };
574
- type ToolHandler<TInput extends ZodRawShapeCompat> = (input: ShapeOutput<TInput>, context: ToolHandlerContext) => Promise<{
575
- /** Text content to return */
576
- text: string;
577
- /** Structured data returned as MCP `structuredContent`. */
578
- data?: Record<string, unknown>;
815
+ type FlowTestResult = (FlowInterruptContent & WithDecodedState) | (FlowWidgetContent & WithDecodedState) | (FlowCompleteContent & WithDecodedState) | (FlowErrorContent & WithDecodedState);
816
+ declare function createFlowTestHarness(flow: RegisteredFlow, options?: {
817
+ stateStore?: FlowStore;
818
+ }): Promise<{
819
+ start(intent: string, stateUpdates?: Record<string, unknown>, context?: string): Promise<FlowTestResult>;
820
+ continueWith(stateUpdates?: Record<string, unknown>): Promise<FlowTestResult>;
821
+ resetWith(stateUpdates: Record<string, unknown>): Promise<FlowTestResult>;
822
+ lastState(): Promise<FlowTokenContent | null>;
579
823
  }>;
580
- type ToolToolCallback<TInput extends ZodRawShapeCompat> = ToolCallback<TInput>;
581
- type RegisteredTool = {
582
- id: string;
583
- title: string;
584
- description: string;
585
- /** Register the tool on the server */
586
- register: (server: McpServer) => Promise<void>;
587
- };
588
824
 
589
825
  /**
590
- * Server-side flow state store.
826
+ * Generic key-value store backed by the WaniWani API.
591
827
  *
592
- * Flow state is stored via the WaniWani API, keyed by session ID.
593
- * Config comes from env vars (WANIWANI_API_KEY, WANIWANI_API_URL).
828
+ * Values are stored as JSON objects (`Record<string, unknown>`) in the
829
+ * `/api/mcp/redis/*` endpoints. Tenant isolation is handled by the API key.
830
+ *
831
+ * Config is read from env vars:
832
+ * - `WANIWANI_API_KEY` (required)
833
+ * - `WANIWANI_API_URL` (optional, defaults to https://app.waniwani.ai)
834
+ * - `WANIWANI_ENCRYPTION_KEY` (optional, base64-encoded 32-byte key for AES-256-GCM encryption)
594
835
  */
595
-
596
- interface FlowStore {
597
- get(key: string): Promise<FlowTokenContent | null>;
598
- set(key: string, value: FlowTokenContent): Promise<void>;
836
+ interface KvStore<T = Record<string, unknown>> {
837
+ get(key: string): Promise<T | null>;
838
+ set(key: string, value: T): Promise<void>;
839
+ delete(key: string): Promise<void>;
840
+ }
841
+ declare class WaniwaniKvStore<T = Record<string, unknown>> implements KvStore<T> {
842
+ private get baseUrl();
843
+ private get apiKey();
844
+ private get encryptionKey();
845
+ get(key: string): Promise<T | null>;
846
+ set(key: string, value: T): Promise<void>;
599
847
  delete(key: string): Promise<void>;
848
+ private request;
600
849
  }
601
850
 
602
- declare const START: "__start__";
603
- declare const END: "__end__";
604
- declare const INTERRUPT: unique symbol;
605
- declare const WIDGET: unique symbol;
606
- /** A single question within an interrupt step */
607
- type InterruptQuestion = {
608
- /** Question to ask the user */
609
- question: string;
610
- /** State key where the answer will be stored */
611
- field: string;
612
- /** Optional suggestions to present as options */
613
- suggestions?: string[];
614
- /** Hidden context/instructions for this specific question (not shown to user directly) */
615
- context?: string;
616
- /** Validation function — runs after the user answers, before advancing to the next node */
617
- validate?: (value: unknown) => MaybePromise<Record<string, unknown> | void>;
618
- };
619
- /**
620
- * Interrupt signal — pauses the flow and asks the user one or more questions.
621
- * Single-question and multi-question interrupts use the same type.
622
- */
623
- type InterruptSignal = {
624
- readonly __type: typeof INTERRUPT;
625
- /** Questions to ask — ask all in one conversational message */
626
- questions: InterruptQuestion[];
627
- /** Overall hidden context/instructions for the assistant (not shown to user directly) */
628
- context?: string;
629
- };
630
- type WidgetSignal = {
631
- readonly __type: typeof WIDGET;
632
- /** The id of the display tool to delegate rendering to */
633
- tool: string;
634
- /** Data to pass to the display tool */
635
- data: Record<string, unknown>;
636
- /** Description of what the widget does (for the AI's context) */
637
- description?: string;
638
- /**
639
- * Whether the user is expected to interact with the widget before the flow continues.
640
- * Defaults to true. Set to false for informational widgets that should render and then
641
- * immediately advance to the next flow step.
642
- */
643
- interactive?: boolean;
644
- /**
645
- * State key this widget fills — enables auto-skip when the field is already in state.
646
- * Pass this so the engine can skip the widget step when the answer is already known.
647
- */
648
- field?: string;
649
- };
650
- type MaybePromise<T> = T | Promise<T>;
651
- /** Deep partial — allows partial updates at any nesting level (for z.object state fields). */
652
- type DeepPartial<T> = {
653
- [K in keyof T]?: T[K] extends Record<string, unknown> ? DeepPartial<T[K]> : T[K];
654
- };
655
- /**
656
- * Extract known (non-index-signature) string keys from a type.
657
- * Zod v4's z.object() adds `[key: string]: unknown` to inferred types,
658
- * so we filter those out to get only the declared field names.
659
- */
660
- type KnownStringKeys<T> = Extract<keyof {
661
- [K in keyof T as string extends K ? never : number extends K ? never : K]: T[K];
662
- }, string>;
663
851
  /**
664
- * Union of all valid field paths for a state type.
665
- * - Flat fields produce their key: `"email"`
666
- * - `z.object()` fields produce dot-paths to sub-fields: `"driver.name"`, `"driver.license"`
667
- * - Arrays and general records (`z.record()`) are treated as flat fields.
668
- * - Only 1 level of nesting is supported.
852
+ * In-memory KvStore implementation.
853
+ *
854
+ * State lives in a `Map` for the lifetime of the process — nothing is
855
+ * persisted, nothing is shared across instances. Use for local development
856
+ * and tests. For production, plug in a Redis/Upstash/CF-KV adapter or set
857
+ * `WANIWANI_API_KEY` to use the hosted store.
669
858
  */
670
- type FieldPaths<TState> = {
671
- [K in Extract<keyof TState, string>]: NonNullable<TState[K]> extends unknown[] ? K : NonNullable<TState[K]> extends Record<string, unknown> ? KnownStringKeys<NonNullable<TState[K]>> extends never ? K : K | `${K}.${KnownStringKeys<NonNullable<TState[K]>>}` : K;
672
- }[Extract<keyof TState, string>];
673
- /** Resolve a dot-path to the value type at that path in TState. */
674
- type ResolveFieldType<TState, P extends string> = P extends `${infer Parent}.${infer Child}` ? Parent extends keyof TState ? Child extends keyof NonNullable<TState[Parent]> ? NonNullable<TState[Parent]>[Child] : never : never : P extends keyof TState ? TState[P] : never;
859
+
860
+ declare class MemoryKvStore<T = Record<string, unknown>> implements KvStore<T> {
861
+ private readonly map;
862
+ get(key: string): Promise<T | null>;
863
+ set(key: string, value: T): Promise<void>;
864
+ delete(key: string): Promise<void>;
865
+ }
866
+
675
867
  /**
676
- * Typed interrupt function available on the node context.
677
- *
678
- * First argument: an object where each key is a field path and each value
679
- * describes the question for that field. Use dot-paths for nested state fields.
680
- * `validate` receives the field's value typed from the Zod schema.
868
+ * Server-side API route handler for widget tracking events.
681
869
  *
682
- * Second argument (optional): config with overall hidden AI instructions.
870
+ * Receives batched events from the `useWaniwani` React hook and forwards them
871
+ * to the WaniWani backend using the server-side SDK.
683
872
  *
684
- * @example
685
- * ```ts
686
- * // Flat field
687
- * interrupt({ breed: { question: "What breed is your pet?" } })
873
+ * @example Next.js App Router
874
+ * ```typescript
875
+ * // app/api/waniwani/track/route.ts
876
+ * import { createTrackingRoute } from "@waniwani/sdk/mcp";
688
877
  *
689
- * // Nested field (z.object in state)
690
- * interrupt({ "driver.name": { question: "Driver's name?" } })
878
+ * const handler = createTrackingRoute({
879
+ * apiKey: process.env.WANIWANI_API_KEY,
880
+ * apiUrl: process.env.WANIWANI_API_URL,
881
+ * });
691
882
  *
692
- * // Multiple questions with context
693
- * interrupt(
694
- * {
695
- * "driver.name": { question: "Name?" },
696
- * "driver.license": { question: "License?" },
697
- * },
698
- * { context: "Ask both questions naturally." },
699
- * )
883
+ * export { handler as POST };
700
884
  * ```
701
885
  */
702
- type TypedInterrupt<TState> = (fields: {
703
- [P in FieldPaths<TState>]?: {
704
- question: string;
705
- validate?: (value: ResolveFieldType<TState, P>) => MaybePromise<DeepPartial<TState> | void>;
706
- suggestions?: string[];
707
- context?: string;
708
- };
709
- }, config?: {
710
- /** Overall hidden context/instructions for the assistant (not shown to user directly) */
711
- context?: string;
712
- }) => InterruptSignal;
713
- /**
714
- * Typed showWidget function — available on the node context.
715
- * The `field` parameter accepts field paths (flat or dot-path for nested state).
716
- */
717
- type TypedShowWidget<TState> = (tool: RegisteredTool | string, config: {
718
- data: Record<string, unknown>;
719
- description?: string;
720
- interactive?: boolean;
721
- field?: FieldPaths<TState>;
722
- }) => WidgetSignal;
723
- /**
724
- * Context object passed to node handlers.
725
- * Provides state, metadata, and typed helper functions for creating signals.
726
- */
727
- type NodeContext<TState> = {
728
- /** Current flow state (partial — fields are filled as the flow progresses) */
729
- state: Partial<TState>;
730
- /** Request metadata from the MCP call */
731
- meta?: Record<string, unknown>;
732
- /** Create an interrupt signal — pause and ask the user questions */
733
- interrupt: TypedInterrupt<TState>;
734
- /** Create a widget signal — pause and show a UI widget */
735
- showWidget: TypedShowWidget<TState>;
736
- /** Session-scoped WaniWani client — available when the server is wrapped with withWaniwani() */
737
- waniwani?: ScopedWaniWaniClient;
738
- };
886
+ interface TrackingRouteOptions {
887
+ /** API key for the WaniWani backend. Defaults to WANIWANI_API_KEY env var. */
888
+ apiKey?: string;
889
+ /** Base URL for the WaniWani backend. Defaults to https://app.waniwani.ai. */
890
+ apiUrl?: string;
891
+ }
739
892
  /**
740
- * Node handler — receives a context object and returns a signal or state updates.
741
- * The return value determines behavior:
742
- * - `Partial<TState>` → action node (state merged, auto-advance)
743
- * - `InterruptSignal` → interrupt (pause, ask user one or more questions)
744
- * - `WidgetSignal` → widget step (pause, show widget)
893
+ * Creates a POST handler that receives tracking events from `useWaniwani`
894
+ * and forwards them to the WaniWani backend.
745
895
  */
746
- type NodeHandler<TState> = (ctx: NodeContext<TState>) => MaybePromise<DeepPartial<TState> | InterruptSignal | WidgetSignal>;
896
+ declare function createTrackingRoute(options?: TrackingRouteOptions): (request: Request) => Promise<Response>;
897
+
747
898
  /**
748
- * Condition function for conditional edges.
749
- * Receives current state, returns the name of the next node.
899
+ * WaniWani SDK Client
900
+ *
901
+ * Extends with each module:
902
+ * - TrackingClient: track(), flush(), shutdown()
903
+ *
904
+ * Pass this client to framework adapters:
905
+ * - `toNextJsHandler(wani, { ... })` for Next.js route handlers
750
906
  */
751
- type ConditionFn<TState> = (state: Partial<TState>) => string | Promise<string>;
752
- type NodeOptions = {
753
- /** Human-readable label for this node (used in funnel visualization and Graphs). */
754
- label: string;
755
- /** When true, this node is excluded from funnel analytics. */
756
- hideFromFunnel?: boolean;
757
- };
907
+ interface WaniWaniClient extends TrackingClient {
908
+ /** @internal Resolved config — used by framework adapters */
909
+ readonly _config: InternalConfig;
910
+ /** Knowledge base client for ingestion, search, and source listing */
911
+ readonly kb: KbClient;
912
+ }
913
+ interface InternalConfig {
914
+ apiUrl: string;
915
+ apiKey: string | undefined;
916
+ tracking: Required<TrackingConfig>;
917
+ }
918
+
919
+ type WaniwaniTracker = Pick<WaniWaniClient, "flush" | "track" | "identify" | "kb" | "_config">;
920
+
921
+ type UnknownRecord = Record<string, unknown>;
758
922
  /**
759
- * Config for the object form of `.addNode({ id, run, label?, hideFromFunnel? })`.
760
- *
761
- * Preferred over the positional form `.addNode(id, run, options?)` — metadata
762
- * sits at the top where the eye lands, and the handler is a named field.
923
+ * Options for withWaniwani().
763
924
  */
764
- type AddNodeConfig<TState, TName extends string = string> = {
765
- /** Unique node id within the flow. */
766
- id: TName;
767
- /** Node handler — receives ctx, returns state updates or a signal. */
768
- run: NodeHandler<TState>;
769
- /** Human-readable label for funnel visualization and graphs. Defaults to `id`. */
770
- label?: string;
771
- /** When true, this node is excluded from funnel analytics. */
772
- hideFromFunnel?: boolean;
773
- };
774
- type FlowGraphNode = {
775
- id: string;
776
- type: "widget" | "interrupt" | "action";
777
- label: string;
778
- hideFromFunnel?: boolean;
779
- };
780
- type FlowGraphEdge = {
781
- from: string;
782
- to: string;
783
- type: "direct";
784
- } | {
785
- from: string;
786
- to: string[];
787
- type: "conditional";
788
- };
789
- type FlowGraph = {
790
- flowId: string;
791
- title: string;
792
- nodes: FlowGraphNode[];
793
- edges: FlowGraphEdge[];
794
- };
795
- type FlowConfig = {
796
- /** Unique identifier for the flow (becomes the MCP tool name) */
797
- id: string;
798
- /** Display title */
799
- title: string;
800
- /** Description for the AI (explains when to use this flow) */
801
- description: string;
925
+ type WithWaniwaniOptions = {
802
926
  /**
803
- * Define the flow's state each field the flow collects.
804
- * Keys are the field names used in `interrupt({ field })`,
805
- * values are Zod schemas with `.describe()`.
927
+ * The WaniWani client instance. When omitted, a client is created
928
+ * automatically using the global config registered by `defineConfig()`,
929
+ * falling back to env vars.
930
+ */
931
+ client?: WaniwaniTracker;
932
+ /**
933
+ * Optional explicit tool type. Defaults to `"other"`.
934
+ */
935
+ toolType?: ToolCalledProperties["type"] | ((toolName: string) => ToolCalledProperties["type"] | undefined);
936
+ /**
937
+ * Optional metadata merged into every tracked event.
938
+ */
939
+ metadata?: UnknownRecord;
940
+ /**
941
+ * Flush tracking transport after each tool call.
942
+ */
943
+ flushAfterToolCall?: boolean;
944
+ /**
945
+ * Optional error callback for non-fatal tracking errors.
946
+ */
947
+ onError?: (error: Error) => void;
948
+ /**
949
+ * Inject widget tracking config into tool response `_meta.waniwani` so browser
950
+ * widgets can send events directly to the WaniWani backend.
806
951
  *
807
- * The state definition serves two purposes:
808
- * 1. Type inference — `TState` is automatically derived, no explicit generic needed
809
- * 2. AI protocol — field names, types, and descriptions are included in the tool
810
- * description so the AI can pre-fill answers via `_meta.flow.state`
952
+ * Always injects `endpoint`. Injects `token` when an API key is configured
953
+ * and token minting succeeds.
811
954
  *
812
- * @example
813
- * ```ts
814
- * state: {
815
- * country: z.string().describe("Country the business is based in"),
816
- * status: z.enum(["registered", "unregistered"]).describe("Business registration status"),
817
- * }
818
- * ```
955
+ * @default true
819
956
  */
820
- state: Record<string, z.ZodType>;
957
+ injectWidgetToken?: boolean;
821
958
  /**
822
- * If true, instruct the agent to omit PII (names, emails, phones, addresses,
823
- * IDs, ages, birthdates) from the `intent` and `context` fields. Only adds a
824
- * guidance line to the tool description does not redact server-side.
959
+ * List of field names to strip from known location `_meta` entries
960
+ * (`openai/userLocation`, `waniwani/geoLocation`, `waniwani/userLocation`)
961
+ * before events are sent to the WaniWani API. Applied to both the
962
+ * request-level `_meta` and any `_meta` on the tool response.
963
+ *
964
+ * Pass e.g. `["latitude", "longitude"]` to drop coordinates only, or
965
+ * `["latitude", "longitude", "city", "region"]` to keep just `country`.
966
+ * Empty/omitted = no redaction.
967
+ *
968
+ * @default []
825
969
  */
826
- omitIntentPII?: boolean;
827
- /** Optional tool annotations */
828
- annotations?: {
829
- readOnlyHint?: boolean;
830
- idempotentHint?: boolean;
831
- openWorldHint?: boolean;
832
- destructiveHint?: boolean;
833
- };
970
+ stripLocationFields?: readonly string[];
971
+ /**
972
+ * Replace `input.stateUpdates[field]` with `"REDACTED"` for any field
973
+ * marked via `redacted()` on a flow state schema. When `false` (default),
974
+ * the declarative markers are ignored and raw values are tracked.
975
+ *
976
+ * Wire this to an env var when you want real values in development logs
977
+ * but redacted values in production.
978
+ *
979
+ * @default false
980
+ */
981
+ applyFieldRedactions?: boolean;
834
982
  };
835
983
  /**
836
- * Infer the runtime state type from a flow's state schema definition.
984
+ * Wrap an MCP server so tool handlers automatically emit `tool.called` events.
837
985
  *
838
- * @example
839
- * ```ts
840
- * const config = {
841
- * state: {
842
- * country: z.enum(["FR", "DE"]),
843
- * status: z.enum(["registered", "unregistered"]),
844
- * }
845
- * };
846
- * type MyState = InferFlowState<typeof config.state>;
847
- * // { country: "FR" | "DE"; status: "registered" | "unregistered" }
848
- * ```
986
+ * The wrapper intercepts `server.registerTool(...)` for future registrations
987
+ * and also walks `server._registeredTools` to wrap any tools already registered
988
+ * at the time of the call. This means either call order works:
989
+ *
990
+ * withWaniwani(server); server.registerTool(...); // wrap then register
991
+ * server.registerTool(...); withWaniwani(server); // register then wrap
992
+ *
993
+ * When `injectWidgetToken` is enabled (default), tracking config is injected
994
+ * into tool response `_meta.waniwani` so browser widgets can post events
995
+ * directly to the WaniWani backend without a server-side proxy.
996
+ *
997
+ * Widget metadata declared on the tool **definition** (e.g. skybridge's
998
+ * `registerWidget`, raw MCP `_meta["ui/resourceUri"]` / `_meta.ui.resourceUri`,
999
+ * OpenAI's `_meta["openai/outputTemplate"]`) is also forwarded into each tool
1000
+ * result's `_meta`, so chat UIs that only see tool results (and not
1001
+ * `tools/list`) can still render widgets. Handler-set keys take precedence.
849
1002
  */
850
- type InferFlowState<T extends Record<string, z.ZodType>> = {
851
- [K in keyof T]: z.infer<T[K]>;
852
- };
1003
+ declare function withWaniwani(server: McpServer, options?: WithWaniwaniOptions): Promise<McpServer>;
1004
+
853
1005
  /**
854
- * Flow tool handler — uses shared MCP types (`RequestHandlerExtra`, `CallToolResult`)
855
- * so it's assignable to both MCP SDK's `ToolCallback` and Skybridge's `ToolHandler`.
1006
+ * Widget platform types
856
1007
  */
857
- type FlowToolHandler = (args: Record<string, unknown>, extra: RequestHandlerExtra<ServerRequest, ServerNotification>) => CallToolResult | Promise<CallToolResult>;
1008
+ type WidgetPlatform = "openai" | "mcp-apps";
858
1009
  /**
859
- * A compiled flow can be registered on an McpServer.
1010
+ * Detects which platform the widget is running on.
860
1011
  *
861
- * Exposes MCP-compatible `name`, `config`, and `handler` so it can be
862
- * registered directly: `server.registerTool(flow.name, flow.config, flow.handler)`
1012
+ * OpenAI injects a global `window.openai` object.
1013
+ * MCP Apps runs in a sandboxed iframe and uses postMessage.
1014
+ *
1015
+ * @deprecated Legacy MCP-widget-in-host stack. Preserved for back-compat; will move to
1016
+ * `@waniwani/sdk/legacy/react` in a future minor release.
863
1017
  */
864
- type RegisteredFlow = {
865
- /** Tool name — pass to `server.registerTool(flow.name, flow.config, flow.handler)`. */
866
- name: string;
867
- /** Tool config object — pass to `server.registerTool(flow.name, flow.config, flow.handler)`. */
868
- config: {
869
- title: string;
870
- description: string;
871
- inputSchema: ZodRawShapeCompat;
872
- annotations?: {
873
- readOnlyHint?: boolean;
874
- idempotentHint?: boolean;
875
- openWorldHint?: boolean;
876
- destructiveHint?: boolean;
877
- };
878
- };
879
- /** Tool callback — pass to `server.registerTool(flow.name, flow.config, flow.handler)`. */
880
- handler: FlowToolHandler;
881
- /** Register this flow on an MCP server. Shorthand for `server.registerTool(flow.name, flow.config, flow.handler)`. */
882
- register: (server: McpServer) => Promise<void>;
883
- /** Returns a Mermaid `flowchart TD` diagram of the flow graph. */
884
- graph: () => string;
885
- flowGraph: FlowGraph;
1018
+ declare function detectPlatform(): WidgetPlatform;
1019
+ /**
1020
+ * Check if running on OpenAI platform.
1021
+ *
1022
+ * @deprecated Legacy MCP-widget-in-host stack. Preserved for back-compat; will move to
1023
+ * `@waniwani/sdk/legacy/react` in a future minor release.
1024
+ */
1025
+ declare function isOpenAI(): boolean;
1026
+ /**
1027
+ * Check if running on MCP Apps platform.
1028
+ *
1029
+ * @deprecated Legacy MCP-widget-in-host stack. Preserved for back-compat; will move to
1030
+ * `@waniwani/sdk/legacy/react` in a future minor release.
1031
+ */
1032
+ declare function isMCPApps(): boolean;
1033
+
1034
+ type ModelContextContentBlock = ContentBlock;
1035
+ type ModelContextUpdate = {
1036
+ content?: ModelContextContentBlock[];
1037
+ structuredContent?: Record<string, unknown>;
886
1038
  };
887
- type FlowTokenContent = {
888
- step?: string;
889
- state: Record<string, unknown>;
890
- field?: string;
891
- widgetId?: string;
1039
+
1040
+ /**
1041
+ * Source: https://github.com/openai/openai-apps-sdk-examples/tree/main/src
1042
+ */
1043
+ type OpenAIGlobals<ToolInput = UnknownObject, ToolOutput = UnknownObject, ToolResponseMetadata = UnknownObject, WidgetState = UnknownObject> = {
1044
+ theme: Theme;
1045
+ userAgent: UserAgent;
1046
+ locale: string;
1047
+ maxHeight: number;
1048
+ displayMode: DisplayMode;
1049
+ safeArea: SafeArea;
1050
+ toolInput: ToolInput;
1051
+ toolOutput: ToolOutput | null;
1052
+ toolResponseMetadata: ToolResponseMetadata | null;
1053
+ widgetState: WidgetState | null;
1054
+ setWidgetState: (state: WidgetState) => Promise<void>;
892
1055
  };
893
- type InterruptQuestionData = {
894
- question: string;
895
- field: string;
896
- suggestions?: string[];
897
- context?: string;
1056
+ type API = {
1057
+ callTool: CallTool;
1058
+ sendFollowUpMessage: (args: {
1059
+ prompt: string;
1060
+ }) => Promise<void>;
1061
+ openExternal(payload: {
1062
+ href: string;
1063
+ }): void;
1064
+ requestDisplayMode: RequestDisplayMode;
898
1065
  };
899
- type FlowInterruptContent = {
900
- status: "interrupt";
901
- /** Single-question shorthand */
902
- question?: string;
903
- field?: string;
904
- suggestions?: string[];
905
- /** Multi-question */
906
- questions?: InterruptQuestionData[];
907
- context?: string;
1066
+ type UnknownObject = Record<string, unknown>;
1067
+ type Theme = "light" | "dark";
1068
+ type SafeAreaInsets = {
1069
+ top: number;
1070
+ bottom: number;
1071
+ left: number;
1072
+ right: number;
908
1073
  };
909
- type FlowWidgetContent = {
910
- status: "widget";
911
- /** Display tool to call */
912
- tool: string;
913
- /** Data to pass to the display tool */
914
- data: Record<string, unknown>;
915
- description?: string;
916
- /** Whether the widget expects user interaction before continuing */
917
- interactive?: boolean;
1074
+ type SafeArea = {
1075
+ insets: SafeAreaInsets;
918
1076
  };
919
- type FlowCompleteContent = {
920
- status: "complete";
1077
+ type DeviceType = "mobile" | "tablet" | "desktop" | "unknown";
1078
+ type UserAgent = {
1079
+ device: {
1080
+ type: DeviceType;
1081
+ };
1082
+ capabilities: {
1083
+ hover: boolean;
1084
+ touch: boolean;
1085
+ };
921
1086
  };
922
- type FlowErrorContent = {
923
- status: "error";
924
- error: string;
1087
+ /** Display mode */
1088
+ type DisplayMode = "pip" | "inline" | "fullscreen";
1089
+ type RequestDisplayMode = (args: {
1090
+ mode: DisplayMode;
1091
+ }) => Promise<{
1092
+ /**
1093
+ * The granted display mode. The host may reject the request.
1094
+ * For mobile, PiP is always coerced to fullscreen.
1095
+ */
1096
+ mode: DisplayMode;
1097
+ }>;
1098
+ type CallToolResponse = {
1099
+ result: string;
925
1100
  };
1101
+ /** Calling APIs */
1102
+ type CallTool = (name: string, args: Record<string, unknown>) => Promise<CallToolResponse>;
1103
+ /** Extra events */
1104
+ declare const SET_GLOBALS_EVENT_TYPE = "openai:set_globals";
1105
+ declare class SetGlobalsEvent extends CustomEvent<{
1106
+ globals: Partial<OpenAIGlobals>;
1107
+ }> {
1108
+ constructor(detail: {
1109
+ globals: Partial<OpenAIGlobals>;
1110
+ });
1111
+ }
1112
+ /**
1113
+ * Global oai object injected by the web sandbox for communicating with chatgpt host page.
1114
+ */
1115
+ declare global {
1116
+ interface Window {
1117
+ openai: API & OpenAIGlobals;
1118
+ innerBaseUrl: string;
1119
+ }
1120
+ interface WindowEventMap {
1121
+ [SET_GLOBALS_EVENT_TYPE]: SetGlobalsEvent;
1122
+ }
1123
+ }
926
1124
 
927
1125
  /**
928
- * A LangGraph-inspired state graph builder for MCP tools.
1126
+ * Result from calling a tool
1127
+ */
1128
+ type ToolCallResult = CallToolResult;
1129
+ /**
1130
+ * Tool result notification (what the host pushes to the widget)
1131
+ */
1132
+ type ToolResult = CallToolResult;
1133
+ /**
1134
+ * Host context - all values available from the host.
1135
+ */
1136
+ type HostContext = {
1137
+ theme: Theme;
1138
+ locale: string;
1139
+ displayMode: DisplayMode;
1140
+ maxHeight: number | null;
1141
+ safeArea: SafeArea | null;
1142
+ toolOutput: UnknownObject | null;
1143
+ toolResponseMetadata: UnknownObject | null;
1144
+ widgetState: UnknownObject | null;
1145
+ };
1146
+ /**
1147
+ * Unified widget client interface that works on both OpenAI and MCP Apps.
929
1148
  *
930
- * @example
931
- * ```ts
932
- * const flow = new StateGraph<MyState>({
933
- * id: "onboarding",
934
- * title: "User Onboarding",
935
- * description: "Guides users through onboarding",
936
- * })
937
- * .addNode({
938
- * id: "ask_name",
939
- * run: ({ interrupt }) => interrupt({ question: "What's your name?", field: "name" }),
940
- * })
941
- * .addNode({
942
- * id: "greet",
943
- * run: ({ state }) => ({ greeting: `Hello ${state.name}!` }),
944
- * })
945
- * .addEdge(START, "ask_name")
946
- * .addEdge("ask_name", "greet")
947
- * .addEdge("greet", END)
948
- * .compile();
949
- * ```
1149
+ * Platform-specific behavior:
1150
+ * - Display mode: OpenAI-only. MCP Apps returns "inline" and requestDisplayMode is a no-op.
1151
+ * - Follow-up messages: Unified API, different underlying implementations.
950
1152
  */
951
- declare class StateGraph<TState extends Record<string, unknown>, TNodes extends string = never> {
952
- private nodes;
953
- private edges;
954
- private nodeOptions;
955
- private config;
956
- constructor(config: FlowConfig);
1153
+ interface UnifiedWidgetClient {
957
1154
  /**
958
- * Add a node with a handler.
959
- *
960
- * The handler receives a context object with `state`, `meta`, `interrupt`, and `showWidget`.
961
- *
962
- * @example
963
- * ```ts
964
- * .addNode({
965
- * id: "ask_name",
966
- * label: "Ask for name",
967
- * run: ({ interrupt }) => interrupt({ question: "What's your name?", field: "name" }),
968
- * })
969
- * ```
1155
+ * Connect to the host. Must be called before using other methods.
1156
+ * On OpenAI, this is a no-op (already connected via window.openai).
1157
+ * On MCP Apps, this establishes the postMessage connection.
970
1158
  */
971
- addNode<TName extends string>(config: AddNodeConfig<TState, TName>): StateGraph<TState, TNodes | TName>;
1159
+ connect(): Promise<void>;
972
1160
  /**
973
- * @deprecated Use the object form: `.addNode({ id, run, label? })`.
974
- * The positional form will be removed in v0.13.0
1161
+ * Close the connection to the host and clean up resources.
1162
+ * On OpenAI, this is a no-op.
1163
+ * On MCP Apps, this closes the transport and removes event listeners.
975
1164
  */
976
- addNode<TName extends string>(name: TName, handler: NodeHandler<TState>, options?: NodeOptions): StateGraph<TState, TNodes | TName>;
1165
+ close(): Promise<void>;
977
1166
  /**
978
- * Add a direct edge between two nodes.
979
- *
980
- * Use `START` as `from` to set the entry point.
981
- * Use `END` as `to` to mark a terminal node.
1167
+ * Get the tool output (structured content returned by the tool handler).
1168
+ * This is the main data source for widget rendering.
982
1169
  */
983
- addEdge(from: typeof START | TNodes, to: TNodes | typeof END): this;
1170
+ getToolOutput<T = Record<string, unknown>>(): T | null;
984
1171
  /**
985
- * Add a conditional edge from a node.
986
- *
987
- * The condition function receives current state and returns the name of the next node.
1172
+ * Register a callback for when tool results are received.
1173
+ * On OpenAI, this subscribes to toolOutput changes.
1174
+ * On MCP Apps, this sets app.ontoolresult.
988
1175
  */
989
- addConditionalEdge(from: TNodes, condition: (state: Partial<TState>) => TNodes | typeof END | Promise<TNodes | typeof END>): this;
1176
+ onToolResult(callback: (result: ToolResult) => void): () => void;
990
1177
  /**
991
- * Generate a Mermaid `flowchart TD` diagram of the graph.
992
- *
993
- * Direct edges use solid arrows. Conditional edges use a dashed arrow
994
- * to a placeholder since branch targets are determined at runtime.
995
- *
996
- * @example
997
- * ```ts
998
- * console.log(graph.graph());
999
- * // flowchart TD
1000
- * // __start__((Start))
1001
- * // ask_name[ask_name]
1002
- * // greet[greet]
1003
- * // __end__((End))
1004
- * // __start__ --> ask_name
1005
- * // ask_name --> greet
1006
- * // greet --> __end__
1007
- * ```
1178
+ * Call another tool on the server.
1179
+ */
1180
+ callTool(name: string, args: Record<string, unknown>): Promise<ToolCallResult>;
1181
+ /**
1182
+ * Open an external URL.
1183
+ * On OpenAI: openai.openExternal({ href })
1184
+ * On MCP Apps: app.sendOpenLink(url)
1185
+ */
1186
+ openExternal(url: string): void;
1187
+ /**
1188
+ * Send a follow-up message to the AI.
1189
+ * On OpenAI: openai.sendFollowUpMessage({ prompt })
1190
+ * On MCP Apps: app.sendMessages([{ role: 'user', content: { type: 'text', text: prompt } }])
1191
+ */
1192
+ sendFollowUp(prompt: string): void | Promise<void>;
1193
+ /**
1194
+ * Update hidden model context for the next assistant turn.
1195
+ * On MCP Apps this uses the standard `ui/update-model-context` request.
1196
+ * On other hosts this may fall back to best-effort behavior.
1197
+ */
1198
+ updateModelContext(context: ModelContextUpdate): Promise<void> | void;
1199
+ /**
1200
+ * Get the current theme.
1201
+ */
1202
+ getTheme(): Theme;
1203
+ /**
1204
+ * Subscribe to theme changes.
1205
+ */
1206
+ onThemeChange(callback: (theme: Theme) => void): () => void;
1207
+ /**
1208
+ * Get the current locale.
1209
+ */
1210
+ getLocale(): string;
1211
+ /**
1212
+ * Get the current display mode.
1213
+ * OpenAI-only: returns "pip" | "inline" | "fullscreen"
1214
+ * MCP Apps: always returns "inline"
1215
+ */
1216
+ getDisplayMode(): DisplayMode;
1217
+ /**
1218
+ * Request a display mode change.
1219
+ * OpenAI-only: requests the mode from the host.
1220
+ * MCP Apps: no-op (returns current mode).
1221
+ */
1222
+ requestDisplayMode(mode: DisplayMode): Promise<DisplayMode>;
1223
+ /**
1224
+ * Subscribe to display mode changes.
1225
+ * OpenAI-only: subscribes to displayMode changes.
1226
+ * MCP Apps: callback is never called.
1227
+ */
1228
+ onDisplayModeChange(callback: (mode: DisplayMode) => void): () => void;
1229
+ /**
1230
+ * Get the safe area insets.
1231
+ * OpenAI-only: returns insets from window.openai.safeArea.
1232
+ * MCP Apps: returns null.
1233
+ */
1234
+ getSafeArea(): SafeArea | null;
1235
+ /**
1236
+ * Subscribe to safe area changes.
1237
+ * OpenAI-only: subscribes to safeArea changes.
1238
+ * MCP Apps: callback is never called.
1239
+ */
1240
+ onSafeAreaChange(callback: (safeArea: SafeArea | null) => void): () => void;
1241
+ /**
1242
+ * Get the max height constraint.
1243
+ * OpenAI-only: returns maxHeight from window.openai.maxHeight.
1244
+ * MCP Apps: returns null.
1245
+ */
1246
+ getMaxHeight(): number | null;
1247
+ /**
1248
+ * Subscribe to max height changes.
1249
+ * OpenAI-only: subscribes to maxHeight changes.
1250
+ * MCP Apps: callback is never called.
1251
+ */
1252
+ onMaxHeightChange(callback: (maxHeight: number | null) => void): () => void;
1253
+ /**
1254
+ * Get the tool response metadata.
1255
+ * OpenAI: returns metadata from window.openai.toolResponseMetadata.
1256
+ * MCP Apps: returns `_meta` from the latest `ui/notifications/tool-result`, if provided by host.
1257
+ */
1258
+ getToolResponseMetadata(): UnknownObject | null;
1259
+ /**
1260
+ * Subscribe to tool response metadata changes.
1261
+ * OpenAI: subscribes to toolResponseMetadata changes.
1262
+ * MCP Apps: fires when tool result `_meta` changes.
1008
1263
  */
1009
- graph(): string;
1264
+ onToolResponseMetadataChange(callback: (metadata: UnknownObject | null) => void): () => void;
1010
1265
  /**
1011
- * Compile the graph into a RegisteredFlow that can be registered on an McpServer.
1012
- *
1013
- * Validates the graph structure and returns a registration-compatible object.
1266
+ * Get the widget state.
1267
+ * OpenAI-only: returns state from window.openai.widgetState.
1268
+ * MCP Apps: returns null.
1014
1269
  */
1015
- compile(options?: {
1016
- store?: FlowStore;
1017
- }): RegisteredFlow;
1018
- private validate;
1019
- }
1020
-
1021
- /**
1022
- * Create a new flow graph — convenience factory for `new StateGraph()`.
1023
- *
1024
- * The state type is automatically inferred from the `state` definition —
1025
- * no explicit generic parameter needed.
1026
- *
1027
- * @example
1028
- * ```ts
1029
- * import { createFlow, interrupt, START, END } from "@waniwani/sdk/mcp";
1030
- * import { z } from "zod";
1031
- *
1032
- * const flow = createFlow({
1033
- * id: "onboarding",
1034
- * title: "User Onboarding",
1035
- * description: "Guides users through onboarding. Use when a user wants to get started.",
1036
- * state: {
1037
- * name: z.string().describe("The user's name"),
1038
- * email: z.string().describe("The user's email address"),
1039
- * },
1040
- * })
1041
- * .addNode({
1042
- * id: "ask_name",
1043
- * run: () => interrupt({ question: "What's your name?", field: "name" }),
1044
- * })
1045
- * .addNode({
1046
- * id: "ask_email",
1047
- * run: () => interrupt({ question: "What's your email?", field: "email" }),
1048
- * })
1049
- * .addEdge(START, "ask_name")
1050
- * .addEdge("ask_name", "ask_email")
1051
- * .addEdge("ask_email", END)
1052
- * .compile();
1053
- * ```
1054
- */
1055
- declare function createFlow<const TSchema extends Record<string, z.ZodType>>(config: Omit<FlowConfig, "state"> & {
1056
- state: TSchema;
1057
- }): StateGraph<InferFlowState<TSchema>>;
1058
-
1059
- /**
1060
- * Mark a Zod schema as PII — its value will be replaced with `"REDACTED"` in
1061
- * any `tool.called` event payload sent to the WaniWani API. The handler still
1062
- * receives the original value; only the tracked copy is scrubbed.
1063
- *
1064
- * Apply at the end of the schema chain so the marker is attached to the final
1065
- * schema:
1066
- *
1067
- * ```ts
1068
- * ages: redacted(z.string().describe("Comma-separated ages")),
1069
- * zipcode: redacted(z.string().describe("Spanish postal code")),
1070
- * ```
1071
- *
1072
- * Uses Zod v4's `.meta()` registry, so the marker is preserved across schema
1073
- * clones (`.optional()`, `.default()`, etc.).
1074
- */
1075
- declare function redacted<T extends z.ZodType>(schema: T): T;
1076
-
1077
- type WithDecodedState = {
1078
- decodedState: FlowTokenContent | null;
1079
- };
1080
- type FlowTestResult = (FlowInterruptContent & WithDecodedState) | (FlowWidgetContent & WithDecodedState) | (FlowCompleteContent & WithDecodedState) | (FlowErrorContent & WithDecodedState);
1081
- declare function createFlowTestHarness(flow: RegisteredFlow, options?: {
1082
- stateStore?: FlowStore;
1083
- }): Promise<{
1084
- start(intent: string, stateUpdates?: Record<string, unknown>, context?: string): Promise<FlowTestResult>;
1085
- continueWith(stateUpdates?: Record<string, unknown>): Promise<FlowTestResult>;
1086
- resetWith(stateUpdates: Record<string, unknown>): Promise<FlowTestResult>;
1087
- lastState(): Promise<FlowTokenContent | null>;
1088
- }>;
1089
-
1090
- /**
1091
- * Generic key-value store backed by the WaniWani API.
1092
- *
1093
- * Values are stored as JSON objects (`Record<string, unknown>`) in the
1094
- * `/api/mcp/redis/*` endpoints. Tenant isolation is handled by the API key.
1095
- *
1096
- * Config is read from env vars:
1097
- * - `WANIWANI_API_KEY` (required)
1098
- * - `WANIWANI_API_URL` (optional, defaults to https://app.waniwani.ai)
1099
- * - `WANIWANI_ENCRYPTION_KEY` (optional, base64-encoded 32-byte key for AES-256-GCM encryption)
1100
- */
1101
- interface KvStore<T = Record<string, unknown>> {
1102
- get(key: string): Promise<T | null>;
1103
- set(key: string, value: T): Promise<void>;
1104
- delete(key: string): Promise<void>;
1105
- }
1106
- declare class WaniwaniKvStore<T = Record<string, unknown>> implements KvStore<T> {
1107
- private get baseUrl();
1108
- private get apiKey();
1109
- private get encryptionKey();
1110
- get(key: string): Promise<T | null>;
1111
- set(key: string, value: T): Promise<void>;
1112
- delete(key: string): Promise<void>;
1113
- private request;
1270
+ getWidgetState(): UnknownObject | null;
1271
+ /**
1272
+ * Subscribe to widget state changes.
1273
+ * OpenAI-only: subscribes to widgetState changes.
1274
+ * MCP Apps: callback is never called.
1275
+ */
1276
+ onWidgetStateChange(callback: (state: UnknownObject | null) => void): () => void;
1114
1277
  }
1115
1278
 
1116
1279
  /**
1117
1280
  * Creates a reusable UI resource (HTML template) that can be attached
1118
1281
  * to tools or flow nodes.
1119
1282
  *
1283
+ * @deprecated Prefer `createFlow` with `showWidget` from `@waniwani/sdk/mcp` for new code.
1284
+ * `createResource` is preserved for back-compat with existing customer MCPs but is no longer
1285
+ * documented; it will move to `@waniwani/sdk/legacy` in a future minor release.
1286
+ *
1120
1287
  * @example
1121
1288
  * ```ts
1122
1289
  * const pricingUI = createResource({
@@ -1138,6 +1305,10 @@ declare function createResource(config: ResourceConfig): RegisteredResource;
1138
1305
  * When `handler()` returns `data`, the tool includes it as MCP `structuredContent`.
1139
1306
  * When `config.resource` is provided, the tool also returns widget metadata.
1140
1307
  *
1308
+ * @deprecated Prefer `createFlow` from `@waniwani/sdk/mcp` for new code. `createTool`
1309
+ * is preserved for back-compat with existing customer MCPs but is no longer documented;
1310
+ * it will move to `@waniwani/sdk/legacy` in a future minor release.
1311
+ *
1141
1312
  * @example
1142
1313
  * ```ts
1143
1314
  * // Widget tool (with resource)
@@ -1163,146 +1334,11 @@ declare function createResource(config: ResourceConfig): RegisteredResource;
1163
1334
  */
1164
1335
  declare function createTool<TInput extends z.ZodRawShape>(config: ToolConfig<TInput>, handler: ToolHandler<TInput>): RegisteredTool;
1165
1336
  /**
1166
- * Registers multiple tools on the server
1167
- */
1168
- declare function registerTools(server: McpServer, tools: RegisteredTool[]): Promise<void>;
1169
-
1170
- /**
1171
- * Server-side API route handler for widget tracking events.
1172
- *
1173
- * Receives batched events from the `useWaniwani` React hook and forwards them
1174
- * to the WaniWani backend using the server-side SDK.
1175
- *
1176
- * @example Next.js App Router
1177
- * ```typescript
1178
- * // app/api/waniwani/track/route.ts
1179
- * import { createTrackingRoute } from "@waniwani/sdk/mcp";
1180
- *
1181
- * const handler = createTrackingRoute({
1182
- * apiKey: process.env.WANIWANI_API_KEY,
1183
- * apiUrl: process.env.WANIWANI_API_URL,
1184
- * });
1185
- *
1186
- * export { handler as POST };
1187
- * ```
1188
- */
1189
- interface TrackingRouteOptions {
1190
- /** API key for the WaniWani backend. Defaults to WANIWANI_API_KEY env var. */
1191
- apiKey?: string;
1192
- /** Base URL for the WaniWani backend. Defaults to https://app.waniwani.ai. */
1193
- apiUrl?: string;
1194
- }
1195
- /**
1196
- * Creates a POST handler that receives tracking events from `useWaniwani`
1197
- * and forwards them to the WaniWani backend.
1198
- */
1199
- declare function createTrackingRoute(options?: TrackingRouteOptions): (request: Request) => Promise<Response>;
1200
-
1201
- /**
1202
- * WaniWani SDK Client
1203
- *
1204
- * Extends with each module:
1205
- * - TrackingClient: track(), flush(), shutdown()
1206
- *
1207
- * Pass this client to framework adapters:
1208
- * - `toNextJsHandler(wani, { ... })` for Next.js route handlers
1209
- */
1210
- interface WaniWaniClient extends TrackingClient {
1211
- /** @internal Resolved config — used by framework adapters */
1212
- readonly _config: InternalConfig;
1213
- /** Knowledge base client for ingestion, search, and source listing */
1214
- readonly kb: KbClient;
1215
- }
1216
- interface InternalConfig {
1217
- apiUrl: string;
1218
- apiKey: string | undefined;
1219
- tracking: Required<TrackingConfig>;
1220
- }
1221
-
1222
- type WaniwaniTracker = Pick<WaniWaniClient, "flush" | "track" | "identify" | "kb" | "_config">;
1223
-
1224
- type UnknownRecord = Record<string, unknown>;
1225
- /**
1226
- * Options for withWaniwani().
1227
- */
1228
- type WithWaniwaniOptions = {
1229
- /**
1230
- * The WaniWani client instance. When omitted, a client is created
1231
- * automatically using the global config registered by `defineConfig()`,
1232
- * falling back to env vars.
1233
- */
1234
- client?: WaniwaniTracker;
1235
- /**
1236
- * Optional explicit tool type. Defaults to `"other"`.
1237
- */
1238
- toolType?: ToolCalledProperties["type"] | ((toolName: string) => ToolCalledProperties["type"] | undefined);
1239
- /**
1240
- * Optional metadata merged into every tracked event.
1241
- */
1242
- metadata?: UnknownRecord;
1243
- /**
1244
- * Flush tracking transport after each tool call.
1245
- */
1246
- flushAfterToolCall?: boolean;
1247
- /**
1248
- * Optional error callback for non-fatal tracking errors.
1249
- */
1250
- onError?: (error: Error) => void;
1251
- /**
1252
- * Inject widget tracking config into tool response `_meta.waniwani` so browser
1253
- * widgets can send events directly to the WaniWani backend.
1254
- *
1255
- * Always injects `endpoint`. Injects `token` when an API key is configured
1256
- * and token minting succeeds.
1257
- *
1258
- * @default true
1259
- */
1260
- injectWidgetToken?: boolean;
1261
- /**
1262
- * List of field names to strip from known location `_meta` entries
1263
- * (`openai/userLocation`, `waniwani/geoLocation`, `waniwani/userLocation`)
1264
- * before events are sent to the WaniWani API. Applied to both the
1265
- * request-level `_meta` and any `_meta` on the tool response.
1266
- *
1267
- * Pass e.g. `["latitude", "longitude"]` to drop coordinates only, or
1268
- * `["latitude", "longitude", "city", "region"]` to keep just `country`.
1269
- * Empty/omitted = no redaction.
1270
- *
1271
- * @default []
1272
- */
1273
- stripLocationFields?: readonly string[];
1274
- /**
1275
- * Replace `input.stateUpdates[field]` with `"REDACTED"` for any field
1276
- * marked via `redacted()` on a flow state schema. When `false` (default),
1277
- * the declarative markers are ignored and raw values are tracked.
1278
- *
1279
- * Wire this to an env var when you want real values in development logs
1280
- * but redacted values in production.
1281
- *
1282
- * @default false
1283
- */
1284
- applyFieldRedactions?: boolean;
1285
- };
1286
- /**
1287
- * Wrap an MCP server so tool handlers automatically emit `tool.called` events.
1288
- *
1289
- * The wrapper intercepts `server.registerTool(...)` for future registrations
1290
- * and also walks `server._registeredTools` to wrap any tools already registered
1291
- * at the time of the call. This means either call order works:
1292
- *
1293
- * withWaniwani(server); server.registerTool(...); // wrap then register
1294
- * server.registerTool(...); withWaniwani(server); // register then wrap
1295
- *
1296
- * When `injectWidgetToken` is enabled (default), tracking config is injected
1297
- * into tool response `_meta.waniwani` so browser widgets can post events
1298
- * directly to the WaniWani backend without a server-side proxy.
1337
+ * Registers multiple tools on the server.
1299
1338
  *
1300
- * Widget metadata declared on the tool **definition** (e.g. skybridge's
1301
- * `registerWidget`, raw MCP `_meta["ui/resourceUri"]` / `_meta.ui.resourceUri`,
1302
- * OpenAI's `_meta["openai/outputTemplate"]`) is also forwarded into each tool
1303
- * result's `_meta`, so chat UIs that only see tool results (and not
1304
- * `tools/list`) can still render widgets. Handler-set keys take precedence.
1339
+ * @deprecated Prefer `createFlow` + `.register()` for new code. Preserved for back-compat
1340
+ * with existing customer MCPs; will move to `@waniwani/sdk/legacy` in a future minor release.
1305
1341
  */
1306
- declare function withWaniwani(server: McpServer, options?: WithWaniwaniOptions): Promise<McpServer>;
1342
+ declare function registerTools(server: McpServer, tools: RegisteredTool[]): Promise<void>;
1307
1343
 
1308
- export { type AddNodeConfig, type ConditionFn, END, type FlowConfig, type FlowTestResult, type HostContext, type InferFlowState, type InterruptSignal, type KvStore, type NodeContext, type NodeHandler, type RegisteredFlow, type RegisteredResource, type RegisteredTool, type ResourceConfig, START, type ScopedWaniWaniClient, StateGraph, type ToolCallResult, type ToolConfig, type ToolHandler, type ToolHandlerContext, type ToolResult, type ToolToolCallback, type TrackingRouteOptions, type TypedInterrupt, type TypedShowWidget, type UnifiedWidgetClient, WaniwaniKvStore, type WidgetCSP, type WidgetPlatform, type WidgetSignal, type WithWaniwaniOptions, createFlow, createFlowTestHarness, createResource, createTool, createTrackingRoute, detectPlatform, isMCPApps, isOpenAI, redacted, registerTools, withWaniwani };
1344
+ export { type AddNodeConfig, type ConditionFn, END, type FlowConfig, type FlowTestResult, type HostContext, type InferFlowState, type InterruptSignal, type KvStore, MemoryKvStore, type NodeContext, type NodeHandler, type RegisteredFlow, type RegisteredResource, type RegisteredTool, type ResourceConfig, START, type ScopedWaniWaniClient, StateGraph, type ToolCallResult, type ToolConfig, type ToolHandler, type ToolHandlerContext, type ToolResult, type ToolToolCallback, type TrackingRouteOptions, type TypedInterrupt, type TypedShowWidget, type UnifiedWidgetClient, WaniwaniKvStore, type WidgetCSP, type WidgetPlatform, type WidgetSignal, type WithWaniwaniOptions, createFlow, createFlowTestHarness, createResource, createTool, createTrackingRoute, detectPlatform, isMCPApps, isOpenAI, redacted, registerTools, withWaniwani };