Skip to content

Commit b47ee89

Browse files
authored
Add support for MCP Apps capabilities and versioning (#56)
* Add host capabilities and version support, including new hooks and types for MCP Apps * Add unit tests for MCP Apps API methods and enhance existing tests for adapters * Enhance host capabilities across MCP and OpenAI adapters, adding comprehensive support for common and platform-specific features. Update tests to reflect new capabilities structure. * Add core dependency and improve error handling in MCP and mock adapters * Enhance MCP and OpenAI adapters to derive host capabilities from runtime context, including available display modes and dynamic feature detection. Update unit tests to validate new capability detection logic. * Enhance README documentation to include host capabilities, bidirectional tools, and theme/style utilities for MCP applications. Update API sections with examples for new features. * Improve error handling in McpAdapter for tool invocation, adding try-catch logic and informative error messages when no handler is registered or an error occurs.
1 parent 3bf5614 commit b47ee89

File tree

23 files changed

+2440
-34
lines changed

23 files changed

+2440
-34
lines changed

packages/ui-react/README.md

Lines changed: 105 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ React widgets often need host-aware APIs for tool calls and UI state. This packa
2828
- Hooks for tools, host context, and widget state
2929
- Typed tool calls with generics
3030
- Optional debug logger hook
31+
- **Host capabilities** - Query what the host supports (theming, display modes, file upload, etc.)
32+
- **Size notifications** - Automatic resize observer integration
33+
- **Partial tool input** - React to streaming tool inputs
3134

3235
## Compatibility
3336

@@ -96,12 +99,109 @@ export function App() {
9699

97100
## API
98101

99-
Key exports include:
102+
### Provider
100103

101-
- `AppsProvider`
102-
- `useAppsClient`, `useToolResult`, `useHostContext`
103-
- `useToolInput`, `useWidgetState`, `useDebugLogger`
104-
- `useFileUpload`, `useFileDownload` (ChatGPT only)
104+
- `AppsProvider` - Context wrapper for all hooks
105+
106+
### Core Hooks
107+
108+
| Hook | Description |
109+
| ---------------- | ----------------------------------------- |
110+
| `useAppsClient` | Client instance for tool calls |
111+
| `useToolResult` | Current tool result data |
112+
| `useToolInput` | Tool input parameters |
113+
| `useHostContext` | Host info (theme, viewport, locale, etc.) |
114+
| `useWidgetState` | Persisted state across reloads |
115+
| `useDisplayMode` | Fullscreen/panel mode control |
116+
| `useDebugLogger` | Debug logging configuration |
117+
118+
### Host Capabilities & Version
119+
120+
```tsx
121+
import { useHostCapabilities, useHostVersion } from "@mcp-apps-kit/ui-react";
122+
123+
function Widget() {
124+
const capabilities = useHostCapabilities();
125+
const version = useHostVersion();
126+
127+
// Common capabilities (both platforms)
128+
const themes = capabilities?.theming?.themes; // ["light", "dark"]
129+
const modes = capabilities?.displayModes?.modes; // ["inline", "fullscreen", "pip"]
130+
131+
// MCP Apps specific
132+
const hasPartialInput = !!capabilities?.partialToolInput;
133+
134+
// ChatGPT specific
135+
const hasFileUpload = !!capabilities?.fileUpload;
136+
137+
// Host version (MCP Apps only)
138+
// { name: "Claude Desktop", version: "1.0.0" }
139+
return <div>Host: {version?.name}</div>;
140+
}
141+
```
142+
143+
### Size Notifications (MCP Apps)
144+
145+
```tsx
146+
import { useSizeChangedNotifications } from "@mcp-apps-kit/ui-react";
147+
148+
function Widget() {
149+
// Attach to container to auto-report size changes
150+
const containerRef = useSizeChangedNotifications();
151+
152+
return <div ref={containerRef}>Content that may resize</div>;
153+
}
154+
```
155+
156+
### Partial Tool Input (MCP Apps)
157+
158+
```tsx
159+
import { useOnToolInputPartial } from "@mcp-apps-kit/ui-react";
160+
161+
function Widget() {
162+
useOnToolInputPartial((input) => {
163+
// React to streaming partial input from the model
164+
console.log("Partial input:", input);
165+
});
166+
167+
return <div>Streaming input widget</div>;
168+
}
169+
```
170+
171+
### Theme & Style Hooks
172+
173+
| Hook | Description |
174+
| ----------------------- | --------------------------------- |
175+
| `useHostStyleVariables` | Apply host-provided CSS variables |
176+
| `useDocumentTheme` | Sync document theme with host |
177+
| `useSafeAreaInsets` | Safe area insets (ChatGPT) |
178+
179+
### Lifecycle Hooks
180+
181+
| Hook | Description |
182+
| -------------------- | -------------------------------------- |
183+
| `useOnToolCancelled` | Callback when tool is cancelled |
184+
| `useOnTeardown` | Cleanup callback before widget removal |
185+
186+
### File Operations (ChatGPT)
187+
188+
| Hook | Description |
189+
| ----------------- | ---------------------- |
190+
| `useFileUpload` | Upload files to host |
191+
| `useFileDownload` | Get file download URLs |
192+
193+
### Layout (ChatGPT)
194+
195+
| Hook | Description |
196+
| -------------------- | --------------------------- |
197+
| `useIntrinsicHeight` | Set widget intrinsic height |
198+
| `useView` | View management |
199+
200+
### Modals (ChatGPT)
201+
202+
| Hook | Description |
203+
| ---------- | ----------------------- |
204+
| `useModal` | Modal dialog management |
105205

106206
## Contributing
107207

packages/ui-react/src/hooks.ts

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import type {
1111
ClientDebugConfig,
1212
ModalOptions,
1313
ModalResult,
14+
HostCapabilities,
15+
HostVersion,
1416
} from "@mcp-apps-kit/ui";
1517
import { clientDebugLogger, type ClientDebugLogger } from "@mcp-apps-kit/ui";
1618
import { useAppsContext } from "./context";
@@ -420,6 +422,172 @@ export function useOnTeardown(handler: (reason?: string) => void): void {
420422
}, [client, handler]);
421423
}
422424

425+
/**
426+
* Subscribe to partial/streaming tool input
427+
*
428+
* Called when the host sends partial tool arguments during streaming.
429+
* Useful for showing real-time input as the user types or as the model generates.
430+
*
431+
* @param handler - Callback for partial input
432+
*
433+
* @example
434+
* ```tsx
435+
* function StreamingInput() {
436+
* const [partialInput, setPartialInput] = useState<Record<string, unknown>>({});
437+
*
438+
* useOnToolInputPartial((input) => {
439+
* setPartialInput(input);
440+
* });
441+
*
442+
* return <pre>{JSON.stringify(partialInput, null, 2)}</pre>;
443+
* }
444+
* ```
445+
*/
446+
export function useOnToolInputPartial(handler: (input: Record<string, unknown>) => void): void {
447+
const { client } = useAppsContext();
448+
449+
useEffect(() => {
450+
if (!client) return;
451+
452+
const unsubscribe = client.onToolInputPartial(handler);
453+
return unsubscribe;
454+
}, [client, handler]);
455+
}
456+
457+
// =============================================================================
458+
// HOST INFORMATION HOOKS
459+
// =============================================================================
460+
461+
/**
462+
* Access host capabilities
463+
*
464+
* Returns the capabilities advertised by the host during handshake.
465+
* Use this to check if features like logging or server tools are supported.
466+
*
467+
* @returns Host capabilities or undefined if not yet connected
468+
*
469+
* @example
470+
* ```tsx
471+
* function CapabilitiesDisplay() {
472+
* const capabilities = useHostCapabilities();
473+
*
474+
* return (
475+
* <div>
476+
* <p>Logging: {capabilities?.logging ? "Supported" : "Not supported"}</p>
477+
* <p>Open Links: {capabilities?.openLinks ? "Supported" : "Not supported"}</p>
478+
* </div>
479+
* );
480+
* }
481+
* ```
482+
*/
483+
export function useHostCapabilities(): HostCapabilities | undefined {
484+
const { client } = useAppsContext();
485+
const [capabilities, setCapabilities] = useState<HostCapabilities | undefined>(
486+
client?.getHostCapabilities()
487+
);
488+
489+
useEffect(() => {
490+
if (!client) return;
491+
setCapabilities(client.getHostCapabilities());
492+
}, [client]);
493+
494+
return capabilities;
495+
}
496+
497+
/**
498+
* Access host version information
499+
*
500+
* Returns the name and version of the host application.
501+
*
502+
* @returns Host version info or undefined if not yet connected
503+
*
504+
* @example
505+
* ```tsx
506+
* function HostInfo() {
507+
* const hostVersion = useHostVersion();
508+
*
509+
* if (!hostVersion) return <div>Loading...</div>;
510+
*
511+
* return (
512+
* <div>
513+
* Running on {hostVersion.name} v{hostVersion.version}
514+
* </div>
515+
* );
516+
* }
517+
* ```
518+
*/
519+
export function useHostVersion(): HostVersion | undefined {
520+
const { client } = useAppsContext();
521+
const [version, setVersion] = useState<HostVersion | undefined>(client?.getHostVersion());
522+
523+
useEffect(() => {
524+
if (!client) return;
525+
setVersion(client.getHostVersion());
526+
}, [client]);
527+
528+
return version;
529+
}
530+
531+
// =============================================================================
532+
// SIZE NOTIFICATION HOOKS
533+
// =============================================================================
534+
535+
/**
536+
* Hook to set up automatic size change notifications
537+
*
538+
* Creates a ResizeObserver that automatically sends size changed
539+
* notifications to the host when the observed element resizes.
540+
*
541+
* @returns Ref to attach to the element to observe
542+
*
543+
* @example
544+
* ```tsx
545+
* function AutoSizeWidget() {
546+
* const containerRef = useSizeChangedNotifications();
547+
*
548+
* return (
549+
* <div ref={containerRef}>
550+
* <p>Content that may change size...</p>
551+
* </div>
552+
* );
553+
* }
554+
* ```
555+
*/
556+
export function useSizeChangedNotifications(): React.RefObject<HTMLElement | null> {
557+
const { client } = useAppsContext();
558+
const containerRef = useRef<HTMLElement | null>(null);
559+
560+
useEffect(() => {
561+
if (!client || typeof ResizeObserver === "undefined") return;
562+
563+
const element = containerRef.current;
564+
if (!element) return;
565+
566+
const observer = new ResizeObserver((entries) => {
567+
for (const entry of entries) {
568+
const { width, height } = entry.contentRect;
569+
void client.sendSizeChanged({
570+
width: Math.round(width),
571+
height: Math.round(height),
572+
});
573+
}
574+
});
575+
576+
observer.observe(element);
577+
578+
// Report initial size
579+
const rect = element.getBoundingClientRect();
580+
void client.sendSizeChanged({
581+
width: Math.round(rect.width),
582+
height: Math.round(rect.height),
583+
});
584+
585+
return () => observer.disconnect();
586+
}, [client]);
587+
588+
return containerRef;
589+
}
590+
423591
// =============================================================================
424592
// FILE OPERATION HOOKS
425593
// =============================================================================

packages/ui-react/src/index.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,19 @@
55
*/
66

77
// Re-export types from @mcp-apps-kit/ui
8-
export type { HostContext, ToolResult, AppsClient } from "@mcp-apps-kit/ui";
8+
export type {
9+
HostContext,
10+
ToolResult,
11+
AppsClient,
12+
// New MCP Apps API types
13+
HostCapabilities,
14+
HostVersion,
15+
AppCapabilities,
16+
SizeChangedParams,
17+
AppToolDefinition,
18+
CallToolHandler,
19+
ListToolsHandler,
20+
} from "@mcp-apps-kit/ui";
921

1022
// Context (placeholder - will be implemented in Phase 6)
1123
export { AppsProvider } from "./context";
@@ -34,6 +46,11 @@ export {
3446
useModal,
3547
// Debug logging
3648
useDebugLogger,
49+
// New MCP Apps API hooks
50+
useOnToolInputPartial,
51+
useHostCapabilities,
52+
useHostVersion,
53+
useSizeChangedNotifications,
3754
} from "./hooks";
3855

3956
// File operation types

0 commit comments

Comments
 (0)