@onereach/idw-apps 0.1.2-beta.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.md +411 -0
  2. package/dist/app/createIdwApp.d.ts +104 -0
  3. package/dist/app/createIdwApp.d.ts.map +1 -0
  4. package/dist/app/createIdwApp.js +151 -0
  5. package/dist/app/createIdwApp.js.map +1 -0
  6. package/dist/app/index.d.ts +15 -0
  7. package/dist/app/index.d.ts.map +1 -0
  8. package/dist/app/index.js +3 -0
  9. package/dist/app/index.js.map +1 -0
  10. package/dist/app/installIdwAppApi.d.ts +98 -0
  11. package/dist/app/installIdwAppApi.d.ts.map +1 -0
  12. package/dist/app/installIdwAppApi.js +314 -0
  13. package/dist/app/installIdwAppApi.js.map +1 -0
  14. package/dist/createIdwAppBridge.d.ts +13 -0
  15. package/dist/createIdwAppBridge.d.ts.map +1 -0
  16. package/dist/createIdwAppBridge.js +555 -0
  17. package/dist/createIdwAppBridge.js.map +1 -0
  18. package/dist/hostStateChannel.d.ts +51 -0
  19. package/dist/hostStateChannel.d.ts.map +1 -0
  20. package/dist/hostStateChannel.js +104 -0
  21. package/dist/hostStateChannel.js.map +1 -0
  22. package/dist/index.d.ts +9 -0
  23. package/dist/index.d.ts.map +1 -0
  24. package/dist/index.js +6 -0
  25. package/dist/index.js.map +1 -0
  26. package/dist/protocol.d.ts +81 -0
  27. package/dist/protocol.d.ts.map +1 -0
  28. package/dist/protocol.js +67 -0
  29. package/dist/protocol.js.map +1 -0
  30. package/dist/stateStore.d.ts +23 -0
  31. package/dist/stateStore.d.ts.map +1 -0
  32. package/dist/stateStore.js +70 -0
  33. package/dist/stateStore.js.map +1 -0
  34. package/dist/types.d.ts +268 -0
  35. package/dist/types.d.ts.map +1 -0
  36. package/dist/types.js +2 -0
  37. package/dist/types.js.map +1 -0
  38. package/package.json +44 -0
  39. package/skills/idw-app-development/SKILL.md +458 -0
package/README.md ADDED
@@ -0,0 +1,411 @@
1
+ # @onereach/idw-apps
2
+
3
+ IDW adapter around [`@modelcontextprotocol/ext-apps`](https://www.npmjs.com/package/@modelcontextprotocol/ext-apps) for hosting **MCP Apps** and embedded IDW web apps (Web Skills, feed widgets, sidebar apps), plus a small iframe-facing IDW/OpenAI-compatible **widget-state** API.
4
+
5
+ This is a thin wrapper, not a replacement or a new framework:
6
+
7
+ - All standard MCP Apps behavior is preserved (tool input/result notifications, app tool calls, server tool calls, server resource/prompt discovery, resource reads, messages, downloads, open-link, logging, size changes, display-mode requests, teardown requests, and model-context updates).
8
+ - On top of that it adds an **IDW app channel** (`window.idw.app`) for durable state and host actions, plus an optional **OpenAI Apps SDK compatibility shim** (`window.openai`).
9
+ - IDW app methods intentionally mirror MCP Apps semantics: `callIdwAction()` feels like `callServerTool()`, and `updateAgentContext()` feels like `updateModelContext()`.
10
+ - It is **framework-agnostic** — no Vue/React. State persistence happens through caller callbacks, never direct store/backend imports.
11
+
12
+ External apps built directly with `@modelcontextprotocol/ext-apps` work with the
13
+ host bridge. They use the standard MCP Apps JSON-RPC transport and do not need
14
+ `@onereach/idw-apps/app`. The IDW channel is additive and ignored by standard
15
+ MCP apps.
16
+
17
+ Current OpenAI Apps SDK apps that use the
18
+ [standard MCP Apps bridge](https://developers.openai.com/apps-sdk/mcp-apps-in-chatgpt#host-bridge)
19
+ have the same compatibility. ChatGPT-only `window.openai` extensions are a separate layer:
20
+ IDW currently provides only the documented widget-state aliases, not the full
21
+ ChatGPT component runtime (`callTool`, file APIs, modals, checkout, and other
22
+ host-specific extensions). Build portable app behavior on MCP Apps and
23
+ feature-detect optional `window.openai` methods.
24
+
25
+ > **Module format:** ESM-only with `bundler` module resolution (required to consume `@modelcontextprotocol/ext-apps`' `exports` map). Both consumers — the IDW host (Vite) and in-iframe apps — are bundled, so this is a deliberate deviation from the CJS+ESM packages in this repo.
26
+
27
+ ## Install
28
+
29
+ ```bash
30
+ pnpm add @onereach/idw-apps
31
+ ```
32
+
33
+ `@onereach/idw-apps` owns the underlying MCP Apps SDK dependency. IDW app authors do not
34
+ install or import `@modelcontextprotocol/ext-apps` directly.
35
+
36
+ ## Two entry points
37
+
38
+ | Import | Runs in | Purpose |
39
+ |--------|---------|---------|
40
+ | `@onereach/idw-apps` | Host (idw-ui) | `createIdwAppBridge` — wrap `AppBridge` for one app iframe |
41
+ | `@onereach/idw-apps/app` | Inside the app iframe | `App` / `createIdwApp` — standard MCP App plus IDW state/actions |
42
+
43
+ ## Host side
44
+
45
+ ```ts
46
+ import { createIdwAppBridge } from '@onereach/idw-apps';
47
+
48
+ const bridge = createIdwAppBridge({
49
+ iframe,
50
+ idwId,
51
+ chatId,
52
+ hostKey, // durable app-instance identity
53
+ surface: 'app-sidebar', // web-app | feed-widget | app-sidebar | chat-inline | fullscreen
54
+ appInstanceId: hostKey,
55
+ auth: {
56
+ getOrToken: (ctx) => issueOrTokenForApp(ctx),
57
+ apiBaseUrl: IDW_API_URL,
58
+ allowTokenRequest: true, // enables lazy callIdwAction({ name: 'getAuthToken' })
59
+ },
60
+ // Include getAuthToken here when an allowlist is present and lazy auth is enabled.
61
+ allowedActions: ['openSkill', 'startConversation', 'getAuthToken'],
62
+ artifactId, // optional: current MCP-backed runtime tool call
63
+ toolCallId, // optional: current MCP-backed tool call id
64
+ initialState, // durable widget/view state previously loaded from your DB
65
+ onStateChange: (state) => persistWidgetState(chatId, hostKey, state), // may be async
66
+ onIdwAction: (params, ctx) => handleIdwAction(params, ctx),
67
+ onCallTool: (params, ctx) => chatApi.callMcpAppTool(ctx.chatId!, ctx.artifactId!, params),
68
+ onListResources: (params, ctx) => chatApi.listMcpAppResources(ctx.chatId!, ctx.artifactId!, params),
69
+ onListResourceTemplates: (params, ctx) => chatApi.listMcpAppResourceTemplates(ctx.chatId!, ctx.artifactId!, params),
70
+ onListPrompts: (params, ctx) => chatApi.listMcpAppPrompts(ctx.chatId!, ctx.artifactId!, params),
71
+ onReadResource: (params, ctx) => chatApi.readMcpAppResource(ctx.chatId!, ctx.artifactId!, params),
72
+ onMessage: (params, ctx) => enqueueAppMessage(ctx.chatId!, ctx.hostKey!, params),
73
+ onDownloadFile: (params, ctx) => downloadFromApp(ctx.hostKey!, params),
74
+ // Keep the original MCP payload: it may be structured-only, in which case
75
+ // `text` is an empty string.
76
+ onUpdateModelContext: (text, params) => queueAppContext(hostKey, { text, params }),
77
+ });
78
+
79
+ await bridge.connect();
80
+
81
+ // New agent turn on the same app — no reconnect, no iframe reload:
82
+ await bridge.updateRuntimeContext({
83
+ artifactId: next.tool_call_id,
84
+ toolCallId: next.tool_call_id,
85
+ toolInput: next.tool_args,
86
+ toolResult: next.tool_result,
87
+ });
88
+
89
+ // on unmount
90
+ await bridge.disconnect();
91
+ ```
92
+
93
+ ### Returned API
94
+
95
+ - `connect()` / `disconnect()`
96
+ - `updateRuntimeContext({ artifactId, toolCallId, toolInput, toolResult })` — update runtime identity without recreating the bridge
97
+ - `sendToolInput(args, toolCallId?)` / `sendToolResult(result, toolCallId?)` — stale `toolCallId` values are ignored
98
+ - `listAppTools()` / `callAppTool(params, options?)` — discover and invoke standard MCP tools supplied by the connected iframe app
99
+ - `getAppCapabilities()` — inspect capabilities declared by the initialized app before discovering optional features such as app tools
100
+ - `sendToolCancelled(reason?)`
101
+ - `sendToolListChanged()` / `sendResourceListChanged()` / `sendPromptListChanged()`
102
+ - `setHostContext(context)` — update standard MCP Apps host context while preserving `hostContext.idw`
103
+ - `getWidgetState()` / `setWidgetState(state)`
104
+ - IDW host context is sent through MCP host context under `hostContext.idw`:
105
+ `{ idwId, surface, appInstanceId, auth: { apiBaseUrl }, allowedActions }`.
106
+ `auth.orToken` is omitted by default.
107
+
108
+ For advanced host integrations, `@onereach/idw-apps` also exports:
109
+
110
+ - `McpAppBridge` — the low-level upstream MCP Apps host bridge
111
+ - `McpAppBridgePostMessageTransport`
112
+
113
+ Use `createIdwAppBridge` for IDW chat apps unless you explicitly need to build
114
+ your own host lifecycle/state adapter.
115
+
116
+ `onOpenIdwApp` and `openIdwAppToolName` remain available only as deprecated
117
+ migration hooks for the legacy `open_idw_app` convention. New integrations
118
+ should use standard MCP tools.
119
+
120
+ ### Host persistence contract
121
+
122
+ `onStateChange` is the only persistence hook. When an app calls
123
+ `window.idw.app.setWidgetState(state)` or `window.openai.setWidgetState(state)`, the
124
+ host wrapper updates its in-memory state immediately and then invokes
125
+ `onStateChange(state)`. Store that value in your DB using the durable `hostKey`
126
+ (usually scoped by `chatId` and user/session). On the next reload, load that DB
127
+ value first and pass it back as `initialState`.
128
+
129
+ The package does not import your DB/client/store directly.
130
+
131
+ ### Artifact / runtime split
132
+
133
+ - `hostKey` identifies the durable app instance — one bridge, one iframe.
134
+ - `artifactId` / `toolCallId` identify the current runtime tool call and may change over the bridge's lifetime.
135
+ - `updateRuntimeContext` updates them **without** recreating `AppBridge` or reloading the iframe, so widget UI state survives new turns.
136
+ - Generic Web Apps can omit runtime tool-call identity and MCP proxy callbacks entirely.
137
+
138
+ ### App-provided tools
139
+
140
+ An iframe app can expose standard MCP tools for the host or agent to call. After
141
+ the app initializes, discover and invoke those tools through the bridge:
142
+
143
+ ```ts
144
+ if (bridge.getAppCapabilities()?.tools) {
145
+ const appTools = await bridge.listAppTools();
146
+
147
+ const result = await bridge.callAppTool({
148
+ name: 'listTasks',
149
+ arguments: {},
150
+ }, {
151
+ timeout: 10_000,
152
+ });
153
+ }
154
+ ```
155
+
156
+ `callAppTool()` is host → app. It is distinct from `onCallTool` and
157
+ `app.callServerTool()`, which are app → MCP-server operations. It delegates to
158
+ the standard MCP Apps `tools/list` and `tools/call` protocol; the package adds
159
+ no IDW-specific action messages.
160
+
161
+ App-provided tools exist only while their iframe is connected. IDW UI/API is
162
+ responsible for deciding which tools become agent-visible, namespacing them by
163
+ app instance, applying product policy/confirmation, and routing an agent call
164
+ to the live bridge. A backend capability that must work without the iframe
165
+ mounted should be a server/backend tool instead.
166
+
167
+ Use the following direction map when choosing an API:
168
+
169
+ | Intent | API |
170
+ | --- | --- |
171
+ | App calls its MCP server | `app.callServerTool()` |
172
+ | IDW host/agent calls a live app tool | `bridge.listAppTools()` / `bridge.callAppTool()` |
173
+ | App requests an IDW host/platform action | `app.callIdwAction()` |
174
+ | App gives concise context to the agent | `app.updateAgentContext()` |
175
+ | App saves a lightweight restore hint | `app.state.setWidgetState()` |
176
+
177
+ ## App side (inside the iframe)
178
+
179
+ ```ts
180
+ import { createIdwApp } from '@onereach/idw-apps/app';
181
+
182
+ const app = createIdwApp({
183
+ appInfo: {
184
+ name: 'Hangman',
185
+ version: '1.0.0',
186
+ },
187
+ });
188
+
189
+ app.ontoolinput = ({ arguments: args }) => {
190
+ // Standard MCP Apps tool input notification.
191
+ };
192
+
193
+ app.ontoolresult = (result) => {
194
+ // Store/render the latest authoritative business snapshot from
195
+ // result.structuredContent.
196
+ };
197
+
198
+ app.state.subscribeWidgetState((state) => {
199
+ // Reapply only a lightweight view/restore hint such as a route or selected ID.
200
+ });
201
+
202
+ await app.connect(); // Connect immediately so tool input/results can arrive.
203
+
204
+ const hostCapabilities = app.getHostCapabilities();
205
+ const hostContext = app.getHostContext();
206
+
207
+ const access = await app.validateAccess({
208
+ url: window.location.href,
209
+ });
210
+
211
+ app.state.getWidgetState(); // -> latest widget state known to the app
212
+ app.state.setWidgetState({ tab: 'chart' }); // replaces the whole widget-state object
213
+ app.state.subscribeWidgetState((state) => {
214
+ // React to local or host-pushed state changes.
215
+ });
216
+
217
+ await app.callIdwAction({
218
+ name: 'openSkill',
219
+ arguments: {
220
+ skillId: 'training',
221
+ },
222
+ });
223
+
224
+ const auth = await app.getAuthToken();
225
+ // auth.token is an OR user identity token for this web app session.
226
+
227
+ await app.updateAgentContext({
228
+ content: [{
229
+ type: 'text',
230
+ text: 'User selected lesson 3 and opened quiz remediation.',
231
+ }],
232
+ structuredContent: {
233
+ selectedLessonId: 'lesson-3',
234
+ currentView: 'quiz-remediation',
235
+ },
236
+ });
237
+
238
+ // OpenAI compatibility shim (aliases only):
239
+ window.openai.widgetState;
240
+ window.openai.setWidgetState({ tab: 'chart' });
241
+ ```
242
+
243
+ For advanced cases, `@onereach/idw-apps/app` also exports:
244
+
245
+ - `App` / `IdwApp` — IDW's subclass of the standard MCP Apps `App`
246
+ - `PostMessageTransport`
247
+ - `installIdwAppApi` — state-only installer, only for custom bootstraps or legacy integrations
248
+
249
+ New apps should use `createIdwApp()` rather than calling `installIdwAppApi()` directly.
250
+
251
+ The iframe API is available only to apps that install this entry point, or to
252
+ HTML resources where the IDW API injects an equivalent bootstrap while proxying
253
+ the resource. A cross-origin app loaded by URL cannot be modified by the host
254
+ after it has loaded.
255
+
256
+ ### App-provided tools
257
+
258
+ Apps can expose standard MCP tools that the host may make available to the
259
+ agent. This works for UI actions (`openRequest`) and app/backend actions
260
+ (`listTasks`) alike; the app owns the implementation.
261
+
262
+ ```ts
263
+ const app = createIdwApp({
264
+ appInfo: {
265
+ name: 'Tasks',
266
+ version: '1.0.0',
267
+ },
268
+ capabilities: {
269
+ tools: {},
270
+ },
271
+ });
272
+
273
+ app.onlisttools = async () => ({
274
+ tools: [{
275
+ name: 'listTasks',
276
+ description: 'List tasks visible to the current user.',
277
+ inputSchema: {
278
+ type: 'object',
279
+ properties: {},
280
+ },
281
+ annotations: {
282
+ readOnlyHint: true,
283
+ },
284
+ }],
285
+ });
286
+
287
+ app.oncalltool = async ({ name, arguments: args }) => {
288
+ if (name !== 'listTasks') {
289
+ return { content: [], isError: true };
290
+ }
291
+
292
+ const tasks = await appBackend.listTasks(args);
293
+ return {
294
+ content: [{ type: 'text', text: `Found ${tasks.length} tasks.` }],
295
+ structuredContent: { tasks },
296
+ };
297
+ };
298
+ ```
299
+
300
+ Register `onlisttools` and `oncalltool` before `app.connect()`. Use accurate
301
+ descriptions and JSON Schema, return MCP `CallToolResult` values, and set tool
302
+ annotations such as `readOnlyHint` or `destructiveHint` honestly. The host—not
303
+ the app—decides whether a declared tool is agent-visible or needs confirmation.
304
+
305
+ ### MCP-backed and Web Apps
306
+
307
+ An IDW Web App is an app that is not backed by an MCP server/resource. It still
308
+ uses the same MCP Apps transport and `createIdwApp()` initialization handshake
309
+ as an MCP-backed app; only its business-data source differs.
310
+
311
+ - A generic Web App normally omits `capabilities`. Add `capabilities.tools` only
312
+ when the app itself exposes MCP-style tools to the host/agent.
313
+ - Use `callServerTool()` and server resource APIs only when the host is proxying
314
+ an MCP server for this app. Otherwise use the app's own backend for business
315
+ APIs and `callIdwAction()` only for IDW host/platform actions.
316
+ - Use `app.validateAccess()` after `app.connect()` when the app needs to verify
317
+ the current user can access the Web App/skill. The host must provide `idwId`,
318
+ `auth.apiBaseUrl`, and lazy token access.
319
+ - The host bridge may omit `artifactId`, `toolCallId`, and MCP proxy callbacks
320
+ for a Web App.
321
+
322
+ ### Business data and widget state
323
+
324
+ - Tool input/result uses standard MCP Apps notifications. A tool that renders or
325
+ updates an app should return the complete current business-data snapshot in
326
+ `structuredContent`. Treat that latest tool result as authoritative and
327
+ re-render from it.
328
+ - Widget state is only a lightweight UI/restore overlay: route, selected ID,
329
+ active tab, draft ID, current step, or an opaque backend `sessionId`.
330
+ - Tool results and widget-state messages are independent. Either can arrive
331
+ first, and widget state can be absent on an app's first render. Register both
332
+ handlers and call `app.connect()` immediately; do not wait for widget state
333
+ before connecting or rendering the initial tool result.
334
+ - When a new tool result arrives, replace business data with that snapshot and
335
+ reapply only still-valid UI state. Never merge widget state into business data.
336
+ - If a restore hint starts a backend fetch, do not let that older fetch overwrite
337
+ a newer tool-result snapshot.
338
+
339
+ ### Widget-state semantics
340
+
341
+ - `stateOptions.initialState` is an iframe-local fallback before the host replies.
342
+ It is useful for custom integrations but is not the durable host snapshot; new
343
+ apps normally leave it unset.
344
+ - `setWidgetState(state)` **replaces** the whole widget-state object and updates local state immediately; the host persists asynchronously via `onStateChange` (the iframe never blocks on persistence).
345
+ - `whenWidgetStateReady()` is an optional signal for code that needs a restore hint
346
+ after `app.connect()`. It resolves on a host reply or the fallback timeout; it
347
+ is never a prerequisite for MCP initialization or first render.
348
+ - `getWidgetState()` returns the latest widget state known to the wrapper.
349
+ - `subscribeWidgetState(listener)` observes both local `setWidgetState` calls and host-pushed widget-state changes.
350
+ - `getState()`, `setState()`, `whenStateReady()`, and `subscribeState()` remain as deprecated aliases for existing apps.
351
+ - Widget state must be **JSON-serializable** and is **lightweight widget/view state only** — never store tool results or authoritative business data as widget state.
352
+
353
+ ### IDW actions and agent context
354
+
355
+ - `callIdwAction({ name, arguments })` requests a host/platform action. It returns a standard MCP `CallToolResult`; errors use `isError: true`.
356
+ - Hosts may pass `allowedActions`. When set, every IDW action—including the
357
+ configured auth-token action—must be included or it is rejected.
358
+ - Action names are host-defined strings. This package does not hardcode `openSkill`, `startConversation`, `close`, or other product actions.
359
+ - `getAuthToken` is the built-in lazy auth action. It only returns a token when
360
+ the host passes `auth.allowTokenRequest: true` and, when an allowlist is set,
361
+ includes that action; otherwise it returns `isError: true`.
362
+ - Hosts can pass `auth.getOrToken(runtimeContext)` to retrieve or generate a
363
+ fresh token for every actual token-action request. The provider receives the
364
+ current live runtime context, takes precedence over `auth.orToken`, and is not
365
+ called during bridge creation or connection. The host bridge does not cache
366
+ provider results.
367
+ - `app.getAuthToken()` is the preferred app-side helper. It requests the OR user
368
+ identity token lazily and reuses the in-memory token for the current web app
369
+ session, so `validateAccess()` and later app API calls do not need duplicate
370
+ token requests. Use `forceRefresh: true` only when the host/app explicitly
371
+ needs to replace the session token.
372
+ - `validateAccess({ url })` is an app-side helper for IDW Web apps/skills. It
373
+ reads `hostContext.idw.idwId` and `hostContext.idw.auth.apiBaseUrl`, requests
374
+ the same cached `orToken` used by `app.getAuthToken()`, then calls
375
+ `POST /idw/:idwId/skills/access` from the app. Call it after `app.connect()`.
376
+ - `updateAgentContext(params)` is a semantic alias for MCP Apps `updateModelContext(params)`. It does not add a custom payload shape.
377
+ - The host callback receives both flattened text and the original MCP params.
378
+ Preserve the original params: a useful update can be structured-only and have
379
+ no text to flatten.
380
+ - Use `updateAgentContext()` only for app-reported context the IDW agent should understand in future turns. Use `sendLog()` for debug/telemetry.
381
+ - Auth currently supports only `orToken`; it is a user identity token issued for
382
+ one web app session. The app wrapper caches it in memory for the app instance.
383
+ The host may provide a static `auth.orToken` or generate one lazily with
384
+ `auth.getOrToken`; the package itself does not mint, refresh, or persist tokens.
385
+
386
+ ### OpenAI compatibility
387
+
388
+ - `window.openai.widgetState` mirrors the latest IDW widget state.
389
+ - `window.openai.setWidgetState(state)` delegates to `window.idw.app.setWidgetState(state)`.
390
+ - Existing `window.openai` fields are preserved; the shim only adds/replaces `widgetState` and `setWidgetState`, then restores previous values on `dispose()`.
391
+ - These are compatibility shims only, not a full OpenAI Apps SDK, and should only be exposed inside MCP app iframes.
392
+ - Apps that depend only on ChatGPT-specific `window.openai` methods are not
393
+ automatically portable to IDW. Their core transport should use MCP Apps;
394
+ optional ChatGPT extensions must be feature-detected.
395
+
396
+ ## Security
397
+
398
+ - Every inbound state message is validated against the target iframe's `contentWindow` (host) / the host window (app) before it is trusted.
399
+ - Every inbound action message uses the same iframe source validation and optional action allowlist before caller code runs.
400
+ - Standard MCP Apps traffic stays on the upstream `PostMessageTransport`; the
401
+ IDW channel ignores MCP JSON-RPC frames and sends host-initiated IDW state
402
+ messages only after the iframe has opted into the IDW channel.
403
+ - IDW/OpenAI APIs are only present in iframes that call `installIdwAppApi`.
404
+ - Tool calls and resource reads are routed through your callbacks, so they keep going through the backend's existing authorization checks.
405
+ - The IDW side channel validates the iframe window and binds a non-opaque app origin when the bridge is created. Sandboxed opaque-origin/srcdoc apps require wildcard targeting, so only enable token access for app resources and configuration the host already trusts.
406
+ - `orToken` is not exposed through host context by default. Prefer lazy token access via `callIdwAction({ name: 'getAuthToken' })` with `auth.allowTokenRequest: true`; also allowlist that action when `allowedActions` is set. `auth.getOrToken` and its resolved values are never serialized into host context. Use `auth.exposeInHostContext: true` only for trusted legacy apps that need eager injection of a static `auth.orToken`; it never invokes the provider.
407
+ - Do not leak `chatId`, auth tokens, or backend URLs into model context unless explicitly required.
408
+
409
+ ## Non-goals
410
+
411
+ Does not replace `@modelcontextprotocol/ext-apps`, does not add custom IDW aliases for MCP tool calls/resource reads (those stay on the standard MCP bridge), does not mint auth tokens, does not migrate RWC/pageData integrations, and does not make the OpenAI Apps SDK the primary abstraction.
@@ -0,0 +1,104 @@
1
+ import { App as McpApp, PostMessageTransport } from '@modelcontextprotocol/ext-apps';
2
+ import type { IdwActionParams, IdwActionResult } from '../types';
3
+ import { type IdwActionCallOptions, type InstalledIdwAppApi, type InstallIdwAppApiOptions } from './installIdwAppApi';
4
+ export type IdwAppInfo = ConstructorParameters<typeof McpApp>[0];
5
+ export type IdwAppCapabilities = ConstructorParameters<typeof McpApp>[1];
6
+ export type IdwAppOptions = ConstructorParameters<typeof McpApp>[2];
7
+ export type IdwAgentContextParams = Parameters<McpApp['updateModelContext']>[0];
8
+ export type IdwStateOptions = InstallIdwAppApiOptions;
9
+ export interface IdwAuthToken {
10
+ token: string;
11
+ tokenType: 'or-token';
12
+ apiBaseUrl?: string;
13
+ }
14
+ export interface GetAuthTokenOptions extends IdwActionCallOptions {
15
+ /** Lazy auth-token action name. Defaults to `getAuthToken`. */
16
+ tokenActionName?: string;
17
+ /** Request a new token instead of reusing the in-memory app-session token. */
18
+ forceRefresh?: boolean;
19
+ }
20
+ export interface ValidateAccessParams {
21
+ /** App URL checked by the IDW API. Defaults to `window.location.href`. */
22
+ url?: string;
23
+ }
24
+ export interface ValidateAccessOptions extends GetAuthTokenOptions {
25
+ /** Fetch implementation override for tests or custom runtimes. */
26
+ fetch?: typeof fetch;
27
+ }
28
+ export interface SkillData {
29
+ slug: string;
30
+ id: string;
31
+ name: string;
32
+ description: string;
33
+ created_at: string;
34
+ meta: {
35
+ skill: {
36
+ advanced: boolean;
37
+ disabled: boolean;
38
+ editable: boolean;
39
+ docUrl?: string;
40
+ };
41
+ chat?: {
42
+ id: string;
43
+ url: string;
44
+ web?: boolean;
45
+ };
46
+ };
47
+ }
48
+ export interface ValidateAccessResult {
49
+ skill: SkillData;
50
+ user?: {
51
+ role: string;
52
+ userId?: string;
53
+ email?: string;
54
+ accountId?: string;
55
+ idwId?: string;
56
+ contactId?: string;
57
+ };
58
+ }
59
+ export interface CreateIdwAppOptions {
60
+ /** App identity sent through the standard MCP Apps initialize handshake. */
61
+ appInfo: IdwAppInfo;
62
+ /** Standard MCP Apps capabilities declared by the iframe app. */
63
+ capabilities?: IdwAppCapabilities;
64
+ /** Standard MCP Apps options, forwarded to `@modelcontextprotocol/ext-apps` App. */
65
+ appOptions?: IdwAppOptions;
66
+ /** IDW/OpenAI-compatible widget-state options. */
67
+ stateOptions?: IdwStateOptions;
68
+ }
69
+ /**
70
+ * IDW's app-side MCP App class.
71
+ *
72
+ * This extends the standard `@modelcontextprotocol/ext-apps` `App` class, so it
73
+ * still performs the normal MCP Apps initialize handshake and exposes
74
+ * `getHostCapabilities()`, `getHostContext()`, `callServerTool()`, etc. IDW adds
75
+ * widget state, host/platform actions, and a semantic agent-context alias. The
76
+ * state API installs `window.idw.app` and the optional `window.openai`
77
+ * compatibility shim.
78
+ */
79
+ export declare class IdwApp extends McpApp {
80
+ readonly state: InstalledIdwAppApi;
81
+ private authToken?;
82
+ private authTokenRequest?;
83
+ constructor(appInfo: IdwAppInfo, capabilities?: IdwAppCapabilities, appOptions?: IdwAppOptions, stateOptions?: IdwStateOptions);
84
+ /** Request an IDW host/platform action. Mirrors MCP `callServerTool()`. */
85
+ callIdwAction(params: IdwActionParams, options?: IdwActionCallOptions): Promise<IdwActionResult>;
86
+ /** Semantic IDW alias for MCP Apps `updateModelContext()`. */
87
+ updateAgentContext(params: IdwAgentContextParams, options?: Parameters<McpApp['updateModelContext']>[1]): ReturnType<McpApp['updateModelContext']>;
88
+ /** Return the OR user identity token for this app session, requesting it lazily once. */
89
+ getAuthToken(options?: GetAuthTokenOptions): Promise<IdwAuthToken>;
90
+ /**
91
+ * Validate that the current user can access this IDW Web app/skill.
92
+ *
93
+ * This intentionally keeps the old app-side `idw-skill` behavior: the app
94
+ * requests a lazy OR token, then calls the IDW API itself. Call after
95
+ * `connect()`, because it depends on MCP host context.
96
+ */
97
+ validateAccess(params?: ValidateAccessParams, options?: ValidateAccessOptions): Promise<ValidateAccessResult>;
98
+ /** Remove IDW globals and close the underlying MCP App transport. */
99
+ dispose(): Promise<void>;
100
+ }
101
+ /** Create an IDW app-side MCP App with widget-state support installed. */
102
+ export declare function createIdwApp(options: CreateIdwAppOptions): IdwApp;
103
+ export { PostMessageTransport };
104
+ //# sourceMappingURL=createIdwApp.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"createIdwApp.d.ts","sourceRoot":"","sources":["../../src/app/createIdwApp.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,151 @@
1
+ import { App as McpApp, PostMessageTransport, } from '@modelcontextprotocol/ext-apps';
2
+ import { installIdwAppApi, } from './installIdwAppApi';
3
+ const DEFAULT_AUTH_TOKEN_ACTION = 'getAuthToken';
4
+ function isRecord(value) {
5
+ return typeof value === 'object' && value !== null;
6
+ }
7
+ function currentUrl(url) {
8
+ const resolved = url ?? globalThis.window?.location?.href;
9
+ if (!resolved) {
10
+ throw new Error('IdwApp.validateAccess: url is required outside a browser window');
11
+ }
12
+ return resolved;
13
+ }
14
+ function validateAccessUrl(apiBaseUrl, idwId) {
15
+ const base = apiBaseUrl.endsWith('/') ? apiBaseUrl : `${apiBaseUrl}/`;
16
+ return new URL(`idw/${encodeURIComponent(idwId)}/skills/access`, base).toString();
17
+ }
18
+ function extractAuthToken(result, apiBaseUrl) {
19
+ const structuredContent = result.structuredContent;
20
+ if (!isRecord(structuredContent) || typeof structuredContent.token !== 'string') {
21
+ throw new Error('IdwApp.getAuthToken: getAuthToken did not return a token');
22
+ }
23
+ if (structuredContent.tokenType !== undefined
24
+ && structuredContent.tokenType !== 'or-token') {
25
+ throw new Error('IdwApp.getAuthToken: getAuthToken returned an unsupported token type');
26
+ }
27
+ return {
28
+ token: structuredContent.token,
29
+ tokenType: 'or-token',
30
+ apiBaseUrl: typeof structuredContent.apiBaseUrl === 'string'
31
+ ? structuredContent.apiBaseUrl
32
+ : apiBaseUrl,
33
+ };
34
+ }
35
+ function extractToolError(result) {
36
+ const content = result.content;
37
+ if (Array.isArray(content)) {
38
+ const text = content
39
+ .map((block) => (isRecord(block) && typeof block.text === 'string' ? block.text : ''))
40
+ .filter(Boolean)
41
+ .join('\n');
42
+ if (text) {
43
+ return text;
44
+ }
45
+ }
46
+ return 'Auth token access failed';
47
+ }
48
+ /**
49
+ * IDW's app-side MCP App class.
50
+ *
51
+ * This extends the standard `@modelcontextprotocol/ext-apps` `App` class, so it
52
+ * still performs the normal MCP Apps initialize handshake and exposes
53
+ * `getHostCapabilities()`, `getHostContext()`, `callServerTool()`, etc. IDW adds
54
+ * widget state, host/platform actions, and a semantic agent-context alias. The
55
+ * state API installs `window.idw.app` and the optional `window.openai`
56
+ * compatibility shim.
57
+ */
58
+ export class IdwApp extends McpApp {
59
+ constructor(appInfo, capabilities, appOptions, stateOptions) {
60
+ super(appInfo, capabilities, appOptions);
61
+ this.state = installIdwAppApi(stateOptions);
62
+ this.onteardown = async () => ({});
63
+ }
64
+ /** Request an IDW host/platform action. Mirrors MCP `callServerTool()`. */
65
+ callIdwAction(params, options) {
66
+ return this.state.callIdwAction(params, options);
67
+ }
68
+ /** Semantic IDW alias for MCP Apps `updateModelContext()`. */
69
+ updateAgentContext(params, options) {
70
+ return this.updateModelContext(params, options);
71
+ }
72
+ /** Return the OR user identity token for this app session, requesting it lazily once. */
73
+ getAuthToken(options = {}) {
74
+ if (!options.forceRefresh && this.authToken) {
75
+ return Promise.resolve(this.authToken);
76
+ }
77
+ if (!options.forceRefresh && this.authTokenRequest) {
78
+ return this.authTokenRequest;
79
+ }
80
+ const hostContext = this.getHostContext();
81
+ const apiBaseUrl = hostContext?.idw?.auth?.apiBaseUrl;
82
+ const request = this.callIdwAction({
83
+ name: options.tokenActionName ?? DEFAULT_AUTH_TOKEN_ACTION,
84
+ }, {
85
+ timeout: options.timeout,
86
+ }).then((tokenResult) => {
87
+ if (tokenResult.isError) {
88
+ throw new Error(`IdwApp.getAuthToken: ${extractToolError(tokenResult)}`);
89
+ }
90
+ const token = extractAuthToken(tokenResult, apiBaseUrl);
91
+ this.authToken = token;
92
+ return token;
93
+ }).finally(() => {
94
+ if (this.authTokenRequest === request) {
95
+ this.authTokenRequest = undefined;
96
+ }
97
+ });
98
+ this.authTokenRequest = request;
99
+ return request;
100
+ }
101
+ /**
102
+ * Validate that the current user can access this IDW Web app/skill.
103
+ *
104
+ * This intentionally keeps the old app-side `idw-skill` behavior: the app
105
+ * requests a lazy OR token, then calls the IDW API itself. Call after
106
+ * `connect()`, because it depends on MCP host context.
107
+ */
108
+ async validateAccess(params = {}, options = {}) {
109
+ const hostContext = this.getHostContext();
110
+ const idw = hostContext?.idw;
111
+ const apiBaseUrl = idw?.auth?.apiBaseUrl;
112
+ const idwId = idw?.idwId;
113
+ if (!apiBaseUrl) {
114
+ throw new Error('IdwApp.validateAccess: host context is missing idw.auth.apiBaseUrl');
115
+ }
116
+ if (!idwId) {
117
+ throw new Error('IdwApp.validateAccess: host context is missing idw.idwId');
118
+ }
119
+ const fetchFn = options.fetch ?? globalThis.fetch;
120
+ if (typeof fetchFn !== 'function') {
121
+ throw new Error('IdwApp.validateAccess: fetch is unavailable');
122
+ }
123
+ const auth = await this.getAuthToken(options);
124
+ const endpoint = validateAccessUrl(apiBaseUrl, idwId);
125
+ const response = await fetchFn(endpoint, {
126
+ method: 'POST',
127
+ headers: {
128
+ 'Content-Type': 'application/json;charset=UTF-8',
129
+ ortoken: auth.token,
130
+ },
131
+ body: JSON.stringify({
132
+ url: currentUrl(params.url),
133
+ }),
134
+ });
135
+ if (!response.ok) {
136
+ throw new Error(`IdwApp.validateAccess: request failed [POST ${endpoint}] - ${response.status} ${response.statusText}`);
137
+ }
138
+ return response.json();
139
+ }
140
+ /** Remove IDW globals and close the underlying MCP App transport. */
141
+ async dispose() {
142
+ this.state.dispose();
143
+ await this.close();
144
+ }
145
+ }
146
+ /** Create an IDW app-side MCP App with widget-state support installed. */
147
+ export function createIdwApp(options) {
148
+ return new IdwApp(options.appInfo, options.capabilities, options.appOptions, options.stateOptions);
149
+ }
150
+ export { PostMessageTransport };
151
+ //# sourceMappingURL=createIdwApp.js.map