@ggui-ai/mcp-apps-react-native 0.0.1-placeholder → 0.9.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.
- package/LICENSE +201 -0
- package/README.md +58 -4
- package/package.json +84 -4
- package/src/McpAppIframe/McpAppIframe.tsx +368 -0
- package/src/McpAppIframe/dispatch.ts +418 -0
- package/src/McpAppIframe/index.ts +15 -0
- package/src/McpAppIframe/types.ts +227 -0
- package/src/chat-helpers/index.ts +12 -0
- package/src/chat-helpers/message-groups.ts +124 -0
- package/src/chat-helpers/render.ts +65 -0
- package/src/chat-helpers/useRafThrottled.ts +34 -0
- package/src/components/ErrorBoundary.tsx +96 -0
- package/src/components/GguiProvider.tsx +188 -0
- package/src/components/UiFeedback.tsx +233 -0
- package/src/components/mcp-apps-bridge.ts +388 -0
- package/src/context/GguiContext.ts +121 -0
- package/src/hooks/useAppState.ts +24 -0
- package/src/index.ts +150 -0
- package/src/invoke/index.ts +8 -0
- package/src/invoke/sse-parse.ts +79 -0
- package/src/invoke/useInvoke.ts +502 -0
- package/src/test-setup.ts +85 -0
- package/src/theme/ThemeProvider.tsx +129 -0
- package/src/theme/index.ts +29 -0
- package/src/theme/tokens.ts +201 -0
- package/src/theme/types.ts +94 -0
- package/src/types/react-test-renderer.d.ts +50 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { createContext, useContext } from 'react';
|
|
2
|
+
import type { AdapterPermissions, PermissionStatus, InterfaceContext, EndUserIdentity, AppDisplayConfig } from '@ggui-ai/protocol';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Open interface — host runtime registry of **client/device adapter
|
|
6
|
+
* implementations**. Empty on purpose: capability packages or host
|
|
7
|
+
* apps augment this interface via TypeScript declaration merging so
|
|
8
|
+
* the slot for each adapter lands with its concrete type.
|
|
9
|
+
*
|
|
10
|
+
* Example (host app or downstream capability package):
|
|
11
|
+
* ```ts
|
|
12
|
+
* declare module '@ggui-ai/mcp-apps-react-native' {
|
|
13
|
+
* interface AdapterRegistry {
|
|
14
|
+
* printer?: PrinterAdapter;
|
|
15
|
+
* }
|
|
16
|
+
* }
|
|
17
|
+
* ```
|
|
18
|
+
*
|
|
19
|
+
* Mirrors {@link AdapterRegistry} in `@ggui-ai/mcp-apps-react`. The augmenting
|
|
20
|
+
* party can augment one or both registries depending on which SDKs
|
|
21
|
+
* the adapter ships against. (Browser-capability hooks — camera,
|
|
22
|
+
* geolocation, clipboard, filePicker, notifications, microphone —
|
|
23
|
+
* are better served by the gadget pattern in `@ggui-ai/gadgets`,
|
|
24
|
+
* which doesn't need a Provider-wired adapter; generated UI imports
|
|
25
|
+
* the hook directly.)
|
|
26
|
+
*/
|
|
27
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
|
28
|
+
export interface AdapterRegistry {}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Shape of the value provided by {@link GguiContext}.
|
|
32
|
+
*
|
|
33
|
+
* Contains app identity, WebSocket configuration, adapter permissions,
|
|
34
|
+
* interface context, and optional auth/session state consumed by hooks.
|
|
35
|
+
* Extends the web version with `reactVersion` and `designSystemUrl` for
|
|
36
|
+
* WebView import map configuration.
|
|
37
|
+
*/
|
|
38
|
+
export interface GguiContextValue {
|
|
39
|
+
appId: string;
|
|
40
|
+
wsEndpoint?: string;
|
|
41
|
+
adapterPermissions: AdapterPermissions;
|
|
42
|
+
requestPermission: (adapter: string) => Promise<PermissionStatus>;
|
|
43
|
+
/** Host-registered adapter implementations, keyed by capability name.
|
|
44
|
+
* Generated UI reads from this registry via {@link useAdapter} (or
|
|
45
|
+
* capability-specific hooks exposed by host apps that augment
|
|
46
|
+
* {@link AdapterRegistry}). Grant decisions live on
|
|
47
|
+
* `clientCapabilities.gadgets[*].permission` — this registry is
|
|
48
|
+
* purely the runtime implementation slot. */
|
|
49
|
+
adapterImpls: AdapterRegistry;
|
|
50
|
+
/** Current interface context (device/viewport info) */
|
|
51
|
+
interfaceContext: InterfaceContext;
|
|
52
|
+
/** Auth context surfaced to embedding hosts and renderer surfaces. */
|
|
53
|
+
auth?: {
|
|
54
|
+
currentUser?: EndUserIdentity;
|
|
55
|
+
userId?: string;
|
|
56
|
+
token?: string;
|
|
57
|
+
isAuthenticated: boolean;
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* Conversation envelope identity. Forwarded by {@link useInvoke} as
|
|
61
|
+
* the `X-Ggui-Host-Session-Id` header so the agent threads multi-turn
|
|
62
|
+
* invokes through its own keyed conversation state. Names the chat
|
|
63
|
+
* thread, not a render.
|
|
64
|
+
*/
|
|
65
|
+
hostSessionId?: string;
|
|
66
|
+
/** React version for WebView import map (default: '18.2.0') */
|
|
67
|
+
reactVersion?: string;
|
|
68
|
+
/** Base URL for design system modules in WebView import map */
|
|
69
|
+
designSystemUrl?: string;
|
|
70
|
+
/**
|
|
71
|
+
* App config (endpointUrl, defaultShellType, etc). Mirrors the web SDK's
|
|
72
|
+
* context field — `useInvoke` reads `endpointUrl` from here when the
|
|
73
|
+
* caller doesn't override it. Populated by {@link GguiProvider} via its
|
|
74
|
+
* `appConfig` prop.
|
|
75
|
+
*/
|
|
76
|
+
appConfig?: AppDisplayConfig | null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* React context that carries ggui configuration to all descendant components.
|
|
81
|
+
*
|
|
82
|
+
* Provided by {@link GguiProvider}. Access the value with {@link useGguiContext}.
|
|
83
|
+
*/
|
|
84
|
+
export const GguiContext = createContext<GguiContextValue | null>(null);
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Access the nearest {@link GguiContext} value.
|
|
88
|
+
*
|
|
89
|
+
* Must be called inside a `<GguiProvider>`. Throws if no provider is found.
|
|
90
|
+
*
|
|
91
|
+
* @returns The current {@link GguiContextValue}
|
|
92
|
+
* @throws Error if called outside a GguiProvider
|
|
93
|
+
*/
|
|
94
|
+
export function useGguiContext(): GguiContextValue {
|
|
95
|
+
const ctx = useContext(GguiContext);
|
|
96
|
+
if (!ctx) {
|
|
97
|
+
throw new Error('useGguiContext must be used within a GguiProvider');
|
|
98
|
+
}
|
|
99
|
+
return ctx;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Read a host-registered adapter implementation by capability name.
|
|
104
|
+
*
|
|
105
|
+
* Returns `undefined` when the host hasn't wired an implementation
|
|
106
|
+
* for this capability — callers MUST handle that case (e.g. render
|
|
107
|
+
* a fallback UI). Grant decisions live on `clientCapabilities.gadgets
|
|
108
|
+
* [*].permission` and surface to the iframe via `Permissions-Policy`;
|
|
109
|
+
* the SDK context just exposes whatever the host wired into
|
|
110
|
+
* `adapterImpls`.
|
|
111
|
+
*
|
|
112
|
+
* Mirrors {@link useAdapter} in `@ggui-ai/mcp-apps-react`. Capability packages
|
|
113
|
+
* augment both {@link AdapterRegistry} surfaces via declaration
|
|
114
|
+
* merging.
|
|
115
|
+
*/
|
|
116
|
+
export function useAdapter<K extends keyof AdapterRegistry>(
|
|
117
|
+
name: K,
|
|
118
|
+
): AdapterRegistry[K] | undefined {
|
|
119
|
+
const ctx = useGguiContext();
|
|
120
|
+
return ctx.adapterImpls[name];
|
|
121
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { AppState, type AppStateStatus } from 'react-native';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Hook to track React Native AppState (active, background, inactive).
|
|
6
|
+
* Useful for pausing/resuming connections and animations.
|
|
7
|
+
*/
|
|
8
|
+
export function useAppState(): AppStateStatus {
|
|
9
|
+
const [appState, setAppState] = useState<AppStateStatus>(AppState.currentState);
|
|
10
|
+
const appStateRef = useRef(AppState.currentState);
|
|
11
|
+
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
const subscription = AppState.addEventListener('change', (nextState) => {
|
|
14
|
+
appStateRef.current = nextState;
|
|
15
|
+
setAppState(nextState);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
return () => {
|
|
19
|
+
subscription.remove();
|
|
20
|
+
};
|
|
21
|
+
}, []);
|
|
22
|
+
|
|
23
|
+
return appState;
|
|
24
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @ggui-ai/mcp-apps-react-native - React Native SDK for ggui
|
|
3
|
+
*
|
|
4
|
+
* Provides React Native components, hooks, and utilities for embedding ggui
|
|
5
|
+
* agent interfaces in mobile applications. The host primitive is
|
|
6
|
+
* `<McpAppIframe>` — the WebView-backed MCP Apps host (RN analog of the
|
|
7
|
+
* web's `<AppRenderer>` from `@mcp-ui/client`) — surrounded by the
|
|
8
|
+
* message-grouping helpers (`chat-helpers` subpath), the Streamable
|
|
9
|
+
* Invoke hook (`useInvoke`), and a React Native theme system that
|
|
10
|
+
* mirrors the web design tokens.
|
|
11
|
+
*
|
|
12
|
+
* @packageDocumentation
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
// Theme System
|
|
16
|
+
export { ThemeProvider, useTheme, buildTheme } from './theme';
|
|
17
|
+
export type { ThemeProviderProps, RNTheme, RNThemeColors, RNThemeSemantic, RNShadow, RNTransitionPreset, RNAccessibility } from './theme';
|
|
18
|
+
export {
|
|
19
|
+
rnColors,
|
|
20
|
+
rnSemantic,
|
|
21
|
+
rnSpacing,
|
|
22
|
+
rnSpacingNamed,
|
|
23
|
+
rnFontSize,
|
|
24
|
+
rnFontWeight,
|
|
25
|
+
rnLineHeight,
|
|
26
|
+
rnFontFamily,
|
|
27
|
+
rnRadius,
|
|
28
|
+
rnShadow,
|
|
29
|
+
rnDuration,
|
|
30
|
+
rnEasing,
|
|
31
|
+
rnTransition,
|
|
32
|
+
rnAccessibility,
|
|
33
|
+
} from './theme';
|
|
34
|
+
|
|
35
|
+
// Re-export transport types from the transport subpath
|
|
36
|
+
export type {
|
|
37
|
+
ConnectionStatus,
|
|
38
|
+
WebSocketMessage,
|
|
39
|
+
WebSocketMessageType,
|
|
40
|
+
} from '@ggui-ai/protocol/transport/websocket';
|
|
41
|
+
|
|
42
|
+
// Re-export types from protocol
|
|
43
|
+
export type {
|
|
44
|
+
ActionEnvelope,
|
|
45
|
+
EventType,
|
|
46
|
+
// Single GguiSession union (ComponentGguiSession, SystemGguiSession, McpAppsGguiSession)
|
|
47
|
+
// keyed by the flat `sessionId`.
|
|
48
|
+
GguiSession,
|
|
49
|
+
ComponentGguiSession,
|
|
50
|
+
SystemGguiSession,
|
|
51
|
+
GguiSessionStatus,
|
|
52
|
+
AdapterPermissions,
|
|
53
|
+
PermissionStatus,
|
|
54
|
+
SubscribePayload,
|
|
55
|
+
AckPayload,
|
|
56
|
+
StreamEnvelope,
|
|
57
|
+
ErrorPayload,
|
|
58
|
+
RenderPayload,
|
|
59
|
+
PropsUpdatePayload,
|
|
60
|
+
ShellType,
|
|
61
|
+
InterfaceContext,
|
|
62
|
+
DeviceCategory,
|
|
63
|
+
EndUserIdentity,
|
|
64
|
+
} from '@ggui-ai/protocol';
|
|
65
|
+
export { BRIDGE_EVENTS, detectInterfaceContext, getDeviceCategory } from '@ggui-ai/protocol';
|
|
66
|
+
|
|
67
|
+
// Invoke protocol message block types — re-exported at root so facade
|
|
68
|
+
// consumers can pull them from the same import path as useInvoke.
|
|
69
|
+
// Parity: identical type re-export block exists on @ggui-ai/mcp-apps-react.
|
|
70
|
+
export type {
|
|
71
|
+
ContentBlock,
|
|
72
|
+
TextBlock,
|
|
73
|
+
ToolUseBlock,
|
|
74
|
+
ToolResultBlock,
|
|
75
|
+
InvokeTurn,
|
|
76
|
+
} from '@ggui-ai/protocol';
|
|
77
|
+
|
|
78
|
+
// ProtocolError typed union — the canonical shape for every failure
|
|
79
|
+
// the renderer classifies outward. `<McpAppIframe onError>` surfaces
|
|
80
|
+
// it; embedding apps pattern-match on `err.kind`. The sibling package
|
|
81
|
+
// `@ggui-ai/iframe-runtime` owns the declaration; the RN SDK re-exports
|
|
82
|
+
// it at parity with `@ggui-ai/mcp-apps-react` so consumers have a single import
|
|
83
|
+
// point per platform.
|
|
84
|
+
export type {
|
|
85
|
+
ProtocolError,
|
|
86
|
+
ProtocolErrorEmitter,
|
|
87
|
+
BootstrapFailureReason,
|
|
88
|
+
// Bootstrap-failure postMessage envelope shape — the parent receives
|
|
89
|
+
// `{type:'ggui:bootstrap-failed', reason, message}` from the iframe /
|
|
90
|
+
// WebView on any pre-renderer or post-renderer boot failure. RN
|
|
91
|
+
// hosts reading this via WebView `onMessage` pattern-match on the
|
|
92
|
+
// `type` tag to classify iframe-origin failures.
|
|
93
|
+
RendererBootFailedMessage,
|
|
94
|
+
} from '@ggui-ai/iframe-runtime';
|
|
95
|
+
|
|
96
|
+
// Provider
|
|
97
|
+
export { GguiProvider, useGguiContext, useAdapter } from './components/GguiProvider';
|
|
98
|
+
export type { GguiProviderProps } from './components/GguiProvider';
|
|
99
|
+
export type { AdapterRegistry } from './context/GguiContext';
|
|
100
|
+
|
|
101
|
+
// Shared host-role MCP-Apps bridge helpers — exported for composition by
|
|
102
|
+
// callers that want to embed the bridge in a custom WebView wrapper
|
|
103
|
+
// (e.g., custom error overlays, in-app navigation headers). The
|
|
104
|
+
// switch implements the canonical set: ui/initialize, tools/call,
|
|
105
|
+
// ping, ui/open-link, ui/resource-teardown.
|
|
106
|
+
export {
|
|
107
|
+
handleHostBridgeRequest,
|
|
108
|
+
buildInjectedBridgeScript,
|
|
109
|
+
buildDeliveryScript,
|
|
110
|
+
NATIVE_BRIDGE_ENVELOPE_KEY,
|
|
111
|
+
} from './components/mcp-apps-bridge';
|
|
112
|
+
export type { HostBridgeContext } from './components/mcp-apps-bridge';
|
|
113
|
+
|
|
114
|
+
// `<McpAppIframe>` — generic MCP Apps iframe host for React Native.
|
|
115
|
+
// Zero ggui-specific coupling; a mirror of the web host exported from
|
|
116
|
+
// `@ggui-ai/mcp-apps-react`. Any MCP Apps host (Claude Desktop,
|
|
117
|
+
// ChatGPT, VS Code, console, third-party playgrounds) uses this to
|
|
118
|
+
// embed a ggui (or any MCP Apps-conformant) session on RN.
|
|
119
|
+
export { McpAppIframe } from './McpAppIframe/index';
|
|
120
|
+
export type {
|
|
121
|
+
McpAppIframeProps,
|
|
122
|
+
McpAppIframeRef,
|
|
123
|
+
McpAppIframeDimensions,
|
|
124
|
+
McpAppIframePermissions,
|
|
125
|
+
} from './McpAppIframe/index';
|
|
126
|
+
|
|
127
|
+
// Error Boundary
|
|
128
|
+
export { ErrorBoundary } from './components/ErrorBoundary';
|
|
129
|
+
export type { ErrorBoundaryProps } from './components/ErrorBoundary';
|
|
130
|
+
|
|
131
|
+
// UI Feedback affordance — host-side render-shell chrome. Hidden
|
|
132
|
+
// entirely unless the host wires `onUiFeedback`; the payload leaves
|
|
133
|
+
// through that callback only (never the agent ↔ UI wire). An
|
|
134
|
+
// in-iframe twin lives in `@ggui-ai/iframe-runtime` (emitting a
|
|
135
|
+
// `ui-feedback` event on the `ggui:observe` seam) — hosts wire
|
|
136
|
+
// exactly ONE of the two surfaces, never both.
|
|
137
|
+
export { UiFeedback } from './components/UiFeedback';
|
|
138
|
+
export type {
|
|
139
|
+
UiFeedbackProps,
|
|
140
|
+
UiFeedbackPayload,
|
|
141
|
+
UiFeedbackVerdict,
|
|
142
|
+
} from './components/UiFeedback';
|
|
143
|
+
|
|
144
|
+
// Streamable Invoke Protocol (v1.1) hook
|
|
145
|
+
export { useInvoke, parseSseStream } from './invoke/index';
|
|
146
|
+
export type { UseInvokeOptions, UseInvokeReturn, ConversationMessage, InvokeError } from './invoke/index';
|
|
147
|
+
|
|
148
|
+
// Hooks
|
|
149
|
+
export { useAppState } from './hooks/useAppState';
|
|
150
|
+
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SSE frame parser for the streamable invoke protocol (React Native).
|
|
3
|
+
*
|
|
4
|
+
* Reads a `Response.body` chunk by chunk, yields parsed + validated
|
|
5
|
+
* `InvokeEvent`s as they arrive. Buffers partial frames across chunk
|
|
6
|
+
* boundaries — chunks from `fetch` do NOT align with `\n\n` separators.
|
|
7
|
+
*
|
|
8
|
+
* This is a near-exact port of the web implementation
|
|
9
|
+
* (`@ggui-ai/mcp-apps-react/src/invoke/sse-parse.ts`). The only platform concern is
|
|
10
|
+
* `TextDecoder`: modern React Native (Hermes, RN 0.74+) ships it natively,
|
|
11
|
+
* and Expo SDK 50+ polyfills it for older runtimes. If consumers target an
|
|
12
|
+
* older setup they must polyfill `TextDecoder` in their entry file.
|
|
13
|
+
*/
|
|
14
|
+
import { invokeEventSchema, type InvokeEvent } from '@ggui-ai/protocol';
|
|
15
|
+
|
|
16
|
+
const DECODER = new TextDecoder();
|
|
17
|
+
const FRAME_SEP = '\n\n';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Async generator yielding one `InvokeEvent` per SSE frame in the stream.
|
|
21
|
+
*
|
|
22
|
+
* Malformed frames (non-JSON data, schema validation failure) are dropped
|
|
23
|
+
* silently — agents emit a wide variety of frames and a single bad one
|
|
24
|
+
* shouldn't kill the whole turn. Caller handles `event: error` for explicit
|
|
25
|
+
* upstream failures.
|
|
26
|
+
*/
|
|
27
|
+
export async function* parseSseStream(
|
|
28
|
+
body: ReadableStream<Uint8Array>,
|
|
29
|
+
signal?: AbortSignal,
|
|
30
|
+
): AsyncGenerator<InvokeEvent> {
|
|
31
|
+
const reader = body.getReader();
|
|
32
|
+
let buffer = '';
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
while (true) {
|
|
36
|
+
if (signal?.aborted) break;
|
|
37
|
+
const { value, done } = await reader.read();
|
|
38
|
+
if (done) break;
|
|
39
|
+
|
|
40
|
+
buffer += DECODER.decode(value, { stream: true });
|
|
41
|
+
|
|
42
|
+
let sepIdx: number;
|
|
43
|
+
while ((sepIdx = buffer.indexOf(FRAME_SEP)) !== -1) {
|
|
44
|
+
const frame = buffer.slice(0, sepIdx);
|
|
45
|
+
buffer = buffer.slice(sepIdx + FRAME_SEP.length);
|
|
46
|
+
const event = parseFrame(frame);
|
|
47
|
+
if (event) yield event;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Flush trailing frame (no terminator) — agent that closed cleanly will
|
|
52
|
+
// have sent message_stop with the separator, but be lenient.
|
|
53
|
+
if (buffer.length > 0) {
|
|
54
|
+
const event = parseFrame(buffer);
|
|
55
|
+
if (event) yield event;
|
|
56
|
+
}
|
|
57
|
+
} finally {
|
|
58
|
+
reader.releaseLock();
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function parseFrame(frame: string): InvokeEvent | null {
|
|
63
|
+
// Frames are `event: NAME\ndata: JSON` (newlines are spec-strict but
|
|
64
|
+
// forgiving here). We only need the data line.
|
|
65
|
+
const dataIdx = frame.indexOf('data: ');
|
|
66
|
+
if (dataIdx === -1) return null;
|
|
67
|
+
const json = frame.slice(dataIdx + 'data: '.length).trim();
|
|
68
|
+
if (!json) return null;
|
|
69
|
+
|
|
70
|
+
let parsed: unknown;
|
|
71
|
+
try {
|
|
72
|
+
parsed = JSON.parse(json);
|
|
73
|
+
} catch {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const result = invokeEventSchema.safeParse(parsed);
|
|
78
|
+
return result.success ? result.data : null;
|
|
79
|
+
}
|