@waniwani/sdk 0.11.10 → 0.11.12

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 +92 -93
  2. package/dist/chat/embed.js +51 -51
  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 +1 -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 +1158 -1080
  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,767 +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;
373
+ /**
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()`.
541
+ *
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
+ * ```
554
+ */
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
+ };
661
+
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);
360
692
  /**
361
- * Ingest files into the knowledge base.
693
+ * Add a node with a handler.
362
694
  *
363
- * **Warning**: This is destructive it deletes ALL existing chunks
364
- * for the environment before ingesting the new files.
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
+ * ```
365
705
  */
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
- }
372
-
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 {
706
+ addNode<TName extends string>(config: AddNodeConfig<TState, TName>): StateGraph<TState, TNodes | TName>;
390
707
  /**
391
- * MCP request metadata passed through to the API.
708
+ * @deprecated Use the object form: `.addNode({ id, run, label? })`.
709
+ * The positional form will be removed in v0.13.0
710
+ */
711
+ addNode<TName extends string>(name: TName, handler: NodeHandler<TState>, options?: NodeOptions): StateGraph<TState, TNodes | TName>;
712
+ /**
713
+ * Add a direct edge between two nodes.
392
714
  *
393
- * Location varies by MCP library:
394
- * - `@vercel/mcp-handler`: `extra._meta`
395
- * - `@modelcontextprotocol/sdk`: `request.params._meta`
715
+ * Use `START` as `from` to set the entry point.
716
+ * Use `END` as `to` to mark a terminal node.
396
717
  */
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;
718
+ addEdge(from: typeof START | TNodes, to: TNodes | typeof END): this;
719
+ /**
720
+ * Add a conditional edge from a node.
721
+ *
722
+ * The condition function receives current state and returns the name of the next node.
723
+ */
724
+ addConditionalEdge(from: TNodes, condition: (state: Partial<TState>) => TNodes | typeof END | Promise<TNodes | typeof END>): this;
725
+ /**
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
+ * ```
743
+ */
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;
410
754
  }
755
+
411
756
  /**
412
- * Modern tracking shape (preferred).
757
+ * Create a new flow graph — convenience factory for `new StateGraph()`.
758
+ *
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
+ * ```
413
789
  */
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);
790
+ declare function createFlow<const TSchema extends Record<string, z.ZodType>>(config: Omit<FlowConfig, "state"> & {
791
+ state: TSchema;
792
+ }): StateGraph<InferFlowState<TSchema>>;
793
+
439
794
  /**
440
- * Legacy tracking shape supported for existing integrations.
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.).
441
809
  */
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
- }
810
+ declare function redacted<T extends z.ZodType>(schema: T): T;
811
+
812
+ type WithDecodedState = {
813
+ decodedState: FlowTokenContent | null;
814
+ };
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>;
823
+ }>;
824
+
453
825
  /**
454
- * Public track input accepted by `client.track()`.
826
+ * Generic key-value store backed by the WaniWani API.
827
+ *
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)
455
835
  */
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;
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>;
477
840
  }
478
- interface TrackingShutdownResult {
479
- timedOut: boolean;
480
- pendingEvents: number;
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>;
847
+ delete(key: string): Promise<void>;
848
+ private request;
481
849
  }
850
+
482
851
  /**
483
- * Tracking module methods for WaniWaniClient.
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.
484
858
  */
485
- interface TrackingClient {
486
- /**
487
- * Send a one-shot identify event for a user.
488
- * userId can be any string: an email, an internal ID, etc.
489
- */
490
- identify: (userId: string, properties?: Record<string, unknown>, meta?: Record<string, unknown>) => Promise<{
491
- eventId: string;
492
- }>;
493
- /**
494
- * Track an event using modern or legacy input shape.
495
- * Returns a deterministic event id immediately after enqueue.
496
- */
497
- track: (event: TrackInput) => Promise<{
498
- eventId: string;
499
- }>;
500
- /**
501
- * Flush all currently buffered events.
502
- */
503
- flush: () => Promise<void>;
504
- /**
505
- * Flush and stop the transport.
506
- */
507
- shutdown: (options?: TrackingShutdownOptions) => Promise<TrackingShutdownResult>;
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>;
508
865
  }
509
866
 
510
867
  /**
511
- * A request-scoped WaniWani client with meta pre-attached.
868
+ * Server-side API route handler for widget tracking events.
512
869
  *
513
- * Available as `context.waniwani` inside `createTool` handlers and flow nodes
514
- * when the server is wrapped with `withWaniwani()`.
870
+ * Receives batched events from the `useWaniwani` React hook and forwards them
871
+ * to the WaniWani backend using the server-side SDK.
872
+ *
873
+ * @example Next.js App Router
874
+ * ```typescript
875
+ * // app/api/waniwani/track/route.ts
876
+ * import { createTrackingRoute } from "@waniwani/sdk/mcp";
877
+ *
878
+ * const handler = createTrackingRoute({
879
+ * apiKey: process.env.WANIWANI_API_KEY,
880
+ * apiUrl: process.env.WANIWANI_API_URL,
881
+ * });
882
+ *
883
+ * export { handler as POST };
884
+ * ```
515
885
  */
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
- };
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;
532
891
  }
533
-
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
- };
573
- };
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>;
579
- }>;
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
- };
892
+ /**
893
+ * Creates a POST handler that receives tracking events from `useWaniwani`
894
+ * and forwards them to the WaniWani backend.
895
+ */
896
+ declare function createTrackingRoute(options?: TrackingRouteOptions): (request: Request) => Promise<Response>;
588
897
 
589
898
  /**
590
- * Server-side flow state store.
899
+ * WaniWani SDK Client
591
900
  *
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).
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
594
906
  */
595
-
596
- interface FlowStore {
597
- get(key: string): Promise<FlowTokenContent | null>;
598
- set(key: string, value: FlowTokenContent): Promise<void>;
599
- delete(key: string): Promise<void>;
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>;
600
917
  }
601
918
 
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
- };
919
+ type WaniwaniTracker = Pick<WaniWaniClient, "flush" | "track" | "identify" | "kb" | "_config">;
920
+
921
+ type UnknownRecord = Record<string, unknown>;
619
922
  /**
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.
923
+ * Options for withWaniwani().
622
924
  */
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;
925
+ type WithWaniwaniOptions = {
638
926
  /**
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.
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.
642
930
  */
643
- interactive?: boolean;
931
+ client?: WaniwaniTracker;
644
932
  /**
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.
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.
951
+ *
952
+ * Always injects `endpoint`. Injects `token` when an API key is configured
953
+ * and token minting succeeds.
954
+ *
955
+ * @default true
956
+ */
957
+ injectWidgetToken?: boolean;
958
+ /**
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 []
969
+ */
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
647
980
  */
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];
981
+ applyFieldRedactions?: boolean;
654
982
  };
655
983
  /**
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.
984
+ * Wrap an MCP server so tool handlers automatically emit `tool.called` events.
985
+ *
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.
659
1002
  */
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>;
1003
+ declare function withWaniwani(server: McpServer, options?: WithWaniwaniOptions): Promise<McpServer>;
1004
+
663
1005
  /**
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.
1006
+ * Widget platform types
669
1007
  */
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;
1008
+ type WidgetPlatform = "openai" | "mcp-apps";
675
1009
  /**
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.
681
- *
682
- * Second argument (optional): config with overall hidden AI instructions.
683
- *
684
- * @example
685
- * ```ts
686
- * // Flat field
687
- * interrupt({ breed: { question: "What breed is your pet?" } })
1010
+ * Detects which platform the widget is running on.
688
1011
  *
689
- * // Nested field (z.object in state)
690
- * interrupt({ "driver.name": { question: "Driver's name?" } })
1012
+ * OpenAI injects a global `window.openai` object.
1013
+ * MCP Apps runs in a sandboxed iframe and uses postMessage.
691
1014
  *
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
- * )
700
- * ```
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.
701
1017
  */
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;
1018
+ declare function detectPlatform(): WidgetPlatform;
713
1019
  /**
714
- * Typed showWidget function — available on the node context.
715
- * The `field` parameter accepts field paths (flat or dot-path for nested state).
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.
716
1024
  */
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;
1025
+ declare function isOpenAI(): boolean;
723
1026
  /**
724
- * Context object passed to node handlers.
725
- * Provides state, metadata, and typed helper functions for creating signals.
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.
726
1031
  */
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;
1032
+ declare function isMCPApps(): boolean;
1033
+
1034
+ type ModelContextContentBlock = ContentBlock;
1035
+ type ModelContextUpdate = {
1036
+ content?: ModelContextContentBlock[];
1037
+ structuredContent?: Record<string, unknown>;
738
1038
  };
1039
+
739
1040
  /**
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)
745
- */
746
- type NodeHandler<TState> = (ctx: NodeContext<TState>) => MaybePromise<DeepPartial<TState> | InterruptSignal | WidgetSignal>;
747
- /**
748
- * Condition function for conditional edges.
749
- * Receives current state, returns the name of the next node.
1041
+ * Source: https://github.com/openai/openai-apps-sdk-examples/tree/main/src
750
1042
  */
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;
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>;
757
1055
  };
758
- type FlowGraphNode = {
759
- id: string;
760
- type: "widget" | "interrupt" | "action";
761
- label: string;
762
- hideFromFunnel?: boolean;
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;
763
1065
  };
764
- type FlowGraphEdge = {
765
- from: string;
766
- to: string;
767
- type: "direct";
768
- } | {
769
- from: string;
770
- to: string[];
771
- type: "conditional";
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;
772
1073
  };
773
- type FlowGraph = {
774
- flowId: string;
775
- title: string;
776
- nodes: FlowGraphNode[];
777
- edges: FlowGraphEdge[];
1074
+ type SafeArea = {
1075
+ insets: SafeAreaInsets;
778
1076
  };
779
- type FlowConfig = {
780
- /** Unique identifier for the flow (becomes the MCP tool name) */
781
- id: string;
782
- /** Display title */
783
- title: string;
784
- /** Description for the AI (explains when to use this flow) */
785
- description: string;
786
- /**
787
- * Define the flow's state — each field the flow collects.
788
- * Keys are the field names used in `interrupt({ field })`,
789
- * values are Zod schemas with `.describe()`.
790
- *
791
- * The state definition serves two purposes:
792
- * 1. Type inference — `TState` is automatically derived, no explicit generic needed
793
- * 2. AI protocol — field names, types, and descriptions are included in the tool
794
- * description so the AI can pre-fill answers via `_meta.flow.state`
795
- *
796
- * @example
797
- * ```ts
798
- * state: {
799
- * country: z.string().describe("Country the business is based in"),
800
- * status: z.enum(["registered", "unregistered"]).describe("Business registration status"),
801
- * }
802
- * ```
803
- */
804
- state: Record<string, z.ZodType>;
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
+ };
1086
+ };
1087
+ /** Display mode */
1088
+ type DisplayMode = "pip" | "inline" | "fullscreen";
1089
+ type RequestDisplayMode = (args: {
1090
+ mode: DisplayMode;
1091
+ }) => Promise<{
805
1092
  /**
806
- * If true, instruct the agent to omit PII (names, emails, phones, addresses,
807
- * IDs, ages, birthdates) from the `intent` and `context` fields. Only adds a
808
- * guidance line to the tool description — does not redact server-side.
1093
+ * The granted display mode. The host may reject the request.
1094
+ * For mobile, PiP is always coerced to fullscreen.
809
1095
  */
810
- omitIntentPII?: boolean;
811
- /** Optional tool annotations */
812
- annotations?: {
813
- readOnlyHint?: boolean;
814
- idempotentHint?: boolean;
815
- openWorldHint?: boolean;
816
- destructiveHint?: boolean;
817
- };
1096
+ mode: DisplayMode;
1097
+ }>;
1098
+ type CallToolResponse = {
1099
+ result: string;
818
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
+ }
1124
+
819
1125
  /**
820
- * Infer the runtime state type from a flow's state schema definition.
821
- *
822
- * @example
823
- * ```ts
824
- * const config = {
825
- * state: {
826
- * country: z.enum(["FR", "DE"]),
827
- * status: z.enum(["registered", "unregistered"]),
828
- * }
829
- * };
830
- * type MyState = InferFlowState<typeof config.state>;
831
- * // { country: "FR" | "DE"; status: "registered" | "unregistered" }
832
- * ```
1126
+ * Result from calling a tool
833
1127
  */
834
- type InferFlowState<T extends Record<string, z.ZodType>> = {
835
- [K in keyof T]: z.infer<T[K]>;
836
- };
1128
+ type ToolCallResult = CallToolResult;
837
1129
  /**
838
- * Flow tool handler uses shared MCP types (`RequestHandlerExtra`, `CallToolResult`)
839
- * so it's assignable to both MCP SDK's `ToolCallback` and Skybridge's `ToolHandler`.
1130
+ * Tool result notification (what the host pushes to the widget)
840
1131
  */
841
- type FlowToolHandler = (args: Record<string, unknown>, extra: RequestHandlerExtra<ServerRequest, ServerNotification>) => CallToolResult | Promise<CallToolResult>;
1132
+ type ToolResult = CallToolResult;
842
1133
  /**
843
- * A compiled flow can be registered on an McpServer.
844
- *
845
- * Exposes MCP-compatible `name`, `config`, and `handler` so it can be
846
- * registered directly: `server.registerTool(flow.name, flow.config, flow.handler)`
1134
+ * Host context - all values available from the host.
847
1135
  */
848
- type RegisteredFlow = {
849
- /** Tool name — pass to `server.registerTool(flow.name, flow.config, flow.handler)`. */
850
- name: string;
851
- /** Tool config object — pass to `server.registerTool(flow.name, flow.config, flow.handler)`. */
852
- config: {
853
- title: string;
854
- description: string;
855
- inputSchema: ZodRawShapeCompat;
856
- annotations?: {
857
- readOnlyHint?: boolean;
858
- idempotentHint?: boolean;
859
- openWorldHint?: boolean;
860
- destructiveHint?: boolean;
861
- };
862
- };
863
- /** Tool callback — pass to `server.registerTool(flow.name, flow.config, flow.handler)`. */
864
- handler: FlowToolHandler;
865
- /** Register this flow on an MCP server. Shorthand for `server.registerTool(flow.name, flow.config, flow.handler)`. */
866
- register: (server: McpServer) => Promise<void>;
867
- /** Returns a Mermaid `flowchart TD` diagram of the flow graph. */
868
- graph: () => string;
869
- flowGraph: FlowGraph;
870
- };
871
- type FlowTokenContent = {
872
- step?: string;
873
- state: Record<string, unknown>;
874
- field?: string;
875
- widgetId?: string;
876
- };
877
- type InterruptQuestionData = {
878
- question: string;
879
- field: string;
880
- suggestions?: string[];
881
- context?: string;
882
- };
883
- type FlowInterruptContent = {
884
- status: "interrupt";
885
- /** Single-question shorthand */
886
- question?: string;
887
- field?: string;
888
- suggestions?: string[];
889
- /** Multi-question */
890
- questions?: InterruptQuestionData[];
891
- context?: string;
892
- };
893
- type FlowWidgetContent = {
894
- status: "widget";
895
- /** Display tool to call */
896
- tool: string;
897
- /** Data to pass to the display tool */
898
- data: Record<string, unknown>;
899
- description?: string;
900
- /** Whether the widget expects user interaction before continuing */
901
- interactive?: boolean;
902
- };
903
- type FlowCompleteContent = {
904
- status: "complete";
905
- };
906
- type FlowErrorContent = {
907
- status: "error";
908
- error: string;
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;
909
1145
  };
910
-
911
1146
  /**
912
- * A LangGraph-inspired state graph builder for MCP tools.
1147
+ * Unified widget client interface that works on both OpenAI and MCP Apps.
913
1148
  *
914
- * @example
915
- * ```ts
916
- * const flow = new StateGraph<MyState>({
917
- * id: "onboarding",
918
- * title: "User Onboarding",
919
- * description: "Guides users through onboarding",
920
- * })
921
- * .addNode("ask_name", ({ interrupt }) => interrupt({ question: "What's your name?", field: "name" }))
922
- * .addNode("greet", ({ state }) => ({ greeting: `Hello ${state.name}!` }))
923
- * .addEdge(START, "ask_name")
924
- * .addEdge("ask_name", "greet")
925
- * .addEdge("greet", END)
926
- * .compile();
927
- * ```
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.
928
1152
  */
929
- declare class StateGraph<TState extends Record<string, unknown>, TNodes extends string = never> {
930
- private nodes;
931
- private edges;
932
- private nodeOptions;
933
- private config;
934
- constructor(config: FlowConfig);
1153
+ interface UnifiedWidgetClient {
935
1154
  /**
936
- * Add a node with a handler.
937
- *
938
- * The handler receives a context object with `state`, `meta`, `interrupt`, and `showWidget`.
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.
939
1158
  */
940
- addNode<TName extends string>(name: TName, handler: NodeHandler<TState>, options?: NodeOptions): StateGraph<TState, TNodes | TName>;
1159
+ connect(): Promise<void>;
941
1160
  /**
942
- * Add a direct edge between two nodes.
943
- *
944
- * Use `START` as `from` to set the entry point.
945
- * Use `END` as `to` to mark a terminal node.
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.
946
1164
  */
947
- addEdge(from: typeof START | TNodes, to: TNodes | typeof END): this;
1165
+ close(): Promise<void>;
948
1166
  /**
949
- * Add a conditional edge from a node.
950
- *
951
- * The condition function receives current state and returns the name of the next node.
1167
+ * Get the tool output (structured content returned by the tool handler).
1168
+ * This is the main data source for widget rendering.
952
1169
  */
953
- addConditionalEdge(from: TNodes, condition: (state: Partial<TState>) => TNodes | typeof END | Promise<TNodes | typeof END>): this;
1170
+ getToolOutput<T = Record<string, unknown>>(): T | null;
954
1171
  /**
955
- * Generate a Mermaid `flowchart TD` diagram of the graph.
956
- *
957
- * Direct edges use solid arrows. Conditional edges use a dashed arrow
958
- * to a placeholder since branch targets are determined at runtime.
959
- *
960
- * @example
961
- * ```ts
962
- * console.log(graph.graph());
963
- * // flowchart TD
964
- * // __start__((Start))
965
- * // ask_name[ask_name]
966
- * // greet[greet]
967
- * // __end__((End))
968
- * // __start__ --> ask_name
969
- * // ask_name --> greet
970
- * // greet --> __end__
971
- * ```
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.
1175
+ */
1176
+ onToolResult(callback: (result: ToolResult) => void): () => void;
1177
+ /**
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.
972
1257
  */
973
- graph(): string;
1258
+ getToolResponseMetadata(): UnknownObject | null;
974
1259
  /**
975
- * Compile the graph into a RegisteredFlow that can be registered on an McpServer.
976
- *
977
- * Validates the graph structure and returns a registration-compatible object.
1260
+ * Subscribe to tool response metadata changes.
1261
+ * OpenAI: subscribes to toolResponseMetadata changes.
1262
+ * MCP Apps: fires when tool result `_meta` changes.
978
1263
  */
979
- compile(options?: {
980
- store?: FlowStore;
981
- }): RegisteredFlow;
982
- private validate;
983
- }
984
-
985
- /**
986
- * Create a new flow graph — convenience factory for `new StateGraph()`.
987
- *
988
- * The state type is automatically inferred from the `state` definition —
989
- * no explicit generic parameter needed.
990
- *
991
- * @example
992
- * ```ts
993
- * import { createFlow, interrupt, START, END } from "@waniwani/sdk/mcp";
994
- * import { z } from "zod";
995
- *
996
- * const flow = createFlow({
997
- * id: "onboarding",
998
- * title: "User Onboarding",
999
- * description: "Guides users through onboarding. Use when a user wants to get started.",
1000
- * state: {
1001
- * name: z.string().describe("The user's name"),
1002
- * email: z.string().describe("The user's email address"),
1003
- * },
1004
- * })
1005
- * .addNode("ask_name", () => interrupt({ question: "What's your name?", field: "name" }))
1006
- * .addNode("ask_email", () => interrupt({ question: "What's your email?", field: "email" }))
1007
- * .addEdge(START, "ask_name")
1008
- * .addEdge("ask_name", "ask_email")
1009
- * .addEdge("ask_email", END)
1010
- * .compile();
1011
- * ```
1012
- */
1013
- declare function createFlow<const TSchema extends Record<string, z.ZodType>>(config: Omit<FlowConfig, "state"> & {
1014
- state: TSchema;
1015
- }): StateGraph<InferFlowState<TSchema>>;
1016
-
1017
- /**
1018
- * Mark a Zod schema as PII — its value will be replaced with `"REDACTED"` in
1019
- * any `tool.called` event payload sent to the WaniWani API. The handler still
1020
- * receives the original value; only the tracked copy is scrubbed.
1021
- *
1022
- * Apply at the end of the schema chain so the marker is attached to the final
1023
- * schema:
1024
- *
1025
- * ```ts
1026
- * ages: redacted(z.string().describe("Comma-separated ages")),
1027
- * zipcode: redacted(z.string().describe("Spanish postal code")),
1028
- * ```
1029
- *
1030
- * Uses Zod v4's `.meta()` registry, so the marker is preserved across schema
1031
- * clones (`.optional()`, `.default()`, etc.).
1032
- */
1033
- declare function redacted<T extends z.ZodType>(schema: T): T;
1034
-
1035
- type WithDecodedState = {
1036
- decodedState: FlowTokenContent | null;
1037
- };
1038
- type FlowTestResult = (FlowInterruptContent & WithDecodedState) | (FlowWidgetContent & WithDecodedState) | (FlowCompleteContent & WithDecodedState) | (FlowErrorContent & WithDecodedState);
1039
- declare function createFlowTestHarness(flow: RegisteredFlow, options?: {
1040
- stateStore?: FlowStore;
1041
- }): Promise<{
1042
- start(intent: string, stateUpdates?: Record<string, unknown>, context?: string): Promise<FlowTestResult>;
1043
- continueWith(stateUpdates?: Record<string, unknown>): Promise<FlowTestResult>;
1044
- resetWith(stateUpdates: Record<string, unknown>): Promise<FlowTestResult>;
1045
- lastState(): Promise<FlowTokenContent | null>;
1046
- }>;
1047
-
1048
- /**
1049
- * Generic key-value store backed by the WaniWani API.
1050
- *
1051
- * Values are stored as JSON objects (`Record<string, unknown>`) in the
1052
- * `/api/mcp/redis/*` endpoints. Tenant isolation is handled by the API key.
1053
- *
1054
- * Config is read from env vars:
1055
- * - `WANIWANI_API_KEY` (required)
1056
- * - `WANIWANI_API_URL` (optional, defaults to https://app.waniwani.ai)
1057
- * - `WANIWANI_ENCRYPTION_KEY` (optional, base64-encoded 32-byte key for AES-256-GCM encryption)
1058
- */
1059
- interface KvStore<T = Record<string, unknown>> {
1060
- get(key: string): Promise<T | null>;
1061
- set(key: string, value: T): Promise<void>;
1062
- delete(key: string): Promise<void>;
1063
- }
1064
- declare class WaniwaniKvStore<T = Record<string, unknown>> implements KvStore<T> {
1065
- private get baseUrl();
1066
- private get apiKey();
1067
- private get encryptionKey();
1068
- get(key: string): Promise<T | null>;
1069
- set(key: string, value: T): Promise<void>;
1070
- delete(key: string): Promise<void>;
1071
- private request;
1264
+ onToolResponseMetadataChange(callback: (metadata: UnknownObject | null) => void): () => void;
1265
+ /**
1266
+ * Get the widget state.
1267
+ * OpenAI-only: returns state from window.openai.widgetState.
1268
+ * MCP Apps: returns null.
1269
+ */
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;
1072
1277
  }
1073
1278
 
1074
1279
  /**
1075
1280
  * Creates a reusable UI resource (HTML template) that can be attached
1076
1281
  * to tools or flow nodes.
1077
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
+ *
1078
1287
  * @example
1079
1288
  * ```ts
1080
1289
  * const pricingUI = createResource({
@@ -1096,6 +1305,10 @@ declare function createResource(config: ResourceConfig): RegisteredResource;
1096
1305
  * When `handler()` returns `data`, the tool includes it as MCP `structuredContent`.
1097
1306
  * When `config.resource` is provided, the tool also returns widget metadata.
1098
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
+ *
1099
1312
  * @example
1100
1313
  * ```ts
1101
1314
  * // Widget tool (with resource)
@@ -1121,146 +1334,11 @@ declare function createResource(config: ResourceConfig): RegisteredResource;
1121
1334
  */
1122
1335
  declare function createTool<TInput extends z.ZodRawShape>(config: ToolConfig<TInput>, handler: ToolHandler<TInput>): RegisteredTool;
1123
1336
  /**
1124
- * Registers multiple tools on the server
1125
- */
1126
- declare function registerTools(server: McpServer, tools: RegisteredTool[]): Promise<void>;
1127
-
1128
- /**
1129
- * Server-side API route handler for widget tracking events.
1130
- *
1131
- * Receives batched events from the `useWaniwani` React hook and forwards them
1132
- * to the WaniWani backend using the server-side SDK.
1133
- *
1134
- * @example Next.js App Router
1135
- * ```typescript
1136
- * // app/api/waniwani/track/route.ts
1137
- * import { createTrackingRoute } from "@waniwani/sdk/mcp";
1138
- *
1139
- * const handler = createTrackingRoute({
1140
- * apiKey: process.env.WANIWANI_API_KEY,
1141
- * apiUrl: process.env.WANIWANI_API_URL,
1142
- * });
1143
- *
1144
- * export { handler as POST };
1145
- * ```
1146
- */
1147
- interface TrackingRouteOptions {
1148
- /** API key for the WaniWani backend. Defaults to WANIWANI_API_KEY env var. */
1149
- apiKey?: string;
1150
- /** Base URL for the WaniWani backend. Defaults to https://app.waniwani.ai. */
1151
- apiUrl?: string;
1152
- }
1153
- /**
1154
- * Creates a POST handler that receives tracking events from `useWaniwani`
1155
- * and forwards them to the WaniWani backend.
1156
- */
1157
- declare function createTrackingRoute(options?: TrackingRouteOptions): (request: Request) => Promise<Response>;
1158
-
1159
- /**
1160
- * WaniWani SDK Client
1161
- *
1162
- * Extends with each module:
1163
- * - TrackingClient: track(), flush(), shutdown()
1164
- *
1165
- * Pass this client to framework adapters:
1166
- * - `toNextJsHandler(wani, { ... })` for Next.js route handlers
1167
- */
1168
- interface WaniWaniClient extends TrackingClient {
1169
- /** @internal Resolved config — used by framework adapters */
1170
- readonly _config: InternalConfig;
1171
- /** Knowledge base client for ingestion, search, and source listing */
1172
- readonly kb: KbClient;
1173
- }
1174
- interface InternalConfig {
1175
- apiUrl: string;
1176
- apiKey: string | undefined;
1177
- tracking: Required<TrackingConfig>;
1178
- }
1179
-
1180
- type WaniwaniTracker = Pick<WaniWaniClient, "flush" | "track" | "identify" | "kb" | "_config">;
1181
-
1182
- type UnknownRecord = Record<string, unknown>;
1183
- /**
1184
- * Options for withWaniwani().
1185
- */
1186
- type WithWaniwaniOptions = {
1187
- /**
1188
- * The WaniWani client instance. When omitted, a client is created
1189
- * automatically using the global config registered by `defineConfig()`,
1190
- * falling back to env vars.
1191
- */
1192
- client?: WaniwaniTracker;
1193
- /**
1194
- * Optional explicit tool type. Defaults to `"other"`.
1195
- */
1196
- toolType?: ToolCalledProperties["type"] | ((toolName: string) => ToolCalledProperties["type"] | undefined);
1197
- /**
1198
- * Optional metadata merged into every tracked event.
1199
- */
1200
- metadata?: UnknownRecord;
1201
- /**
1202
- * Flush tracking transport after each tool call.
1203
- */
1204
- flushAfterToolCall?: boolean;
1205
- /**
1206
- * Optional error callback for non-fatal tracking errors.
1207
- */
1208
- onError?: (error: Error) => void;
1209
- /**
1210
- * Inject widget tracking config into tool response `_meta.waniwani` so browser
1211
- * widgets can send events directly to the WaniWani backend.
1212
- *
1213
- * Always injects `endpoint`. Injects `token` when an API key is configured
1214
- * and token minting succeeds.
1215
- *
1216
- * @default true
1217
- */
1218
- injectWidgetToken?: boolean;
1219
- /**
1220
- * List of field names to strip from known location `_meta` entries
1221
- * (`openai/userLocation`, `waniwani/geoLocation`, `waniwani/userLocation`)
1222
- * before events are sent to the WaniWani API. Applied to both the
1223
- * request-level `_meta` and any `_meta` on the tool response.
1224
- *
1225
- * Pass e.g. `["latitude", "longitude"]` to drop coordinates only, or
1226
- * `["latitude", "longitude", "city", "region"]` to keep just `country`.
1227
- * Empty/omitted = no redaction.
1228
- *
1229
- * @default []
1230
- */
1231
- stripLocationFields?: readonly string[];
1232
- /**
1233
- * Replace `input.stateUpdates[field]` with `"REDACTED"` for any field
1234
- * marked via `redacted()` on a flow state schema. When `false` (default),
1235
- * the declarative markers are ignored and raw values are tracked.
1236
- *
1237
- * Wire this to an env var when you want real values in development logs
1238
- * but redacted values in production.
1239
- *
1240
- * @default false
1241
- */
1242
- applyFieldRedactions?: boolean;
1243
- };
1244
- /**
1245
- * Wrap an MCP server so tool handlers automatically emit `tool.called` events.
1246
- *
1247
- * The wrapper intercepts `server.registerTool(...)` for future registrations
1248
- * and also walks `server._registeredTools` to wrap any tools already registered
1249
- * at the time of the call. This means either call order works:
1250
- *
1251
- * withWaniwani(server); server.registerTool(...); // wrap then register
1252
- * server.registerTool(...); withWaniwani(server); // register then wrap
1253
- *
1254
- * When `injectWidgetToken` is enabled (default), tracking config is injected
1255
- * into tool response `_meta.waniwani` so browser widgets can post events
1256
- * directly to the WaniWani backend without a server-side proxy.
1337
+ * Registers multiple tools on the server.
1257
1338
  *
1258
- * Widget metadata declared on the tool **definition** (e.g. skybridge's
1259
- * `registerWidget`, raw MCP `_meta["ui/resourceUri"]` / `_meta.ui.resourceUri`,
1260
- * OpenAI's `_meta["openai/outputTemplate"]`) is also forwarded into each tool
1261
- * result's `_meta`, so chat UIs that only see tool results (and not
1262
- * `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.
1263
1341
  */
1264
- declare function withWaniwani(server: McpServer, options?: WithWaniwaniOptions): Promise<McpServer>;
1342
+ declare function registerTools(server: McpServer, tools: RegisteredTool[]): Promise<void>;
1265
1343
 
1266
- export { 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 };