@mcp-web/app 0.1.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/dist/create-app.d.ts +160 -0
- package/dist/create-app.d.ts.map +1 -0
- package/dist/create-app.js +94 -0
- package/dist/index.d.ts +74 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +70 -0
- package/dist/internal.d.ts +12 -0
- package/dist/internal.d.ts.map +1 -0
- package/dist/internal.js +10 -0
- package/dist/mcp-app-context.d.ts +122 -0
- package/dist/mcp-app-context.d.ts.map +1 -0
- package/dist/mcp-app-context.js +140 -0
- package/dist/mcp-app-context.test.d.ts +2 -0
- package/dist/mcp-app-context.test.d.ts.map +1 -0
- package/dist/mcp-app-context.test.js +254 -0
- package/dist/render-mcp-app.d.ts +71 -0
- package/dist/render-mcp-app.d.ts.map +1 -0
- package/dist/render-mcp-app.js +87 -0
- package/dist/use-mcp-app-props.d.ts +116 -0
- package/dist/use-mcp-app-props.d.ts.map +1 -0
- package/dist/use-mcp-app-props.js +158 -0
- package/dist/vite-plugin.d.ts +123 -0
- package/dist/vite-plugin.d.ts.map +1 -0
- package/dist/vite-plugin.js +450 -0
- package/package.json +53 -0
- package/src/create-app.test.ts +181 -0
- package/src/create-app.ts +213 -0
- package/src/index.ts +87 -0
- package/src/internal.ts +12 -0
- package/src/mcp-app-context.test.tsx +323 -0
- package/src/mcp-app-context.tsx +183 -0
- package/src/render-mcp-app.tsx +141 -0
- package/src/use-mcp-app-props.ts +166 -0
- package/src/vite-plugin.test.ts +110 -0
- package/src/vite-plugin.ts +620 -0
- package/tsconfig.json +32 -0
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createContext,
|
|
3
|
+
useCallback,
|
|
4
|
+
useContext,
|
|
5
|
+
useState,
|
|
6
|
+
type ReactNode,
|
|
7
|
+
} from 'react';
|
|
8
|
+
import { useApp, useHostStyles, useDocumentTheme } from '@modelcontextprotocol/ext-apps/react';
|
|
9
|
+
import type { App, McpUiHostContext } from '@modelcontextprotocol/ext-apps';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Value provided by `MCPAppProvider`.
|
|
13
|
+
*
|
|
14
|
+
* Contains the ext-apps `App` instance, connection state, and the
|
|
15
|
+
* current host context (theme, styles, display mode, locale, etc.).
|
|
16
|
+
*/
|
|
17
|
+
export interface MCPAppContextValue {
|
|
18
|
+
/** The connected ext-apps `App` instance, null during initialization */
|
|
19
|
+
app: App | null;
|
|
20
|
+
/** Whether initialization completed successfully */
|
|
21
|
+
isConnected: boolean;
|
|
22
|
+
/** Connection error if initialization failed, null otherwise */
|
|
23
|
+
error: Error | null;
|
|
24
|
+
/** Current host context, undefined until connected */
|
|
25
|
+
hostContext: McpUiHostContext | undefined;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const MCPAppContext = createContext<MCPAppContextValue | null>(null);
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Provider that creates the ext-apps `App` instance and applies host styles.
|
|
32
|
+
*
|
|
33
|
+
* This provider:
|
|
34
|
+
* - Creates and manages the `App` connection via `useApp()`
|
|
35
|
+
* - Automatically applies the host's CSS custom properties, theme, and fonts
|
|
36
|
+
* - Tracks host context changes (theme toggles, display mode changes, etc.)
|
|
37
|
+
* - Makes the app instance and host context available to child components
|
|
38
|
+
*
|
|
39
|
+
* @internal Used by `renderMCPApp` — not typically used directly.
|
|
40
|
+
*/
|
|
41
|
+
export function MCPAppProvider({ children }: { children: ReactNode }) {
|
|
42
|
+
const [hostContext, setHostContext] = useState<
|
|
43
|
+
McpUiHostContext | undefined
|
|
44
|
+
>(undefined);
|
|
45
|
+
|
|
46
|
+
const onAppCreated = useCallback((app: App) => {
|
|
47
|
+
app.onhostcontextchanged = (params) => {
|
|
48
|
+
setHostContext((prev) => ({ ...prev, ...params }));
|
|
49
|
+
};
|
|
50
|
+
}, []);
|
|
51
|
+
|
|
52
|
+
const { app, isConnected, error } = useApp({
|
|
53
|
+
appInfo: {
|
|
54
|
+
name: 'mcp-web-app',
|
|
55
|
+
version: '0.1.0',
|
|
56
|
+
},
|
|
57
|
+
capabilities: {},
|
|
58
|
+
onAppCreated,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// Capture initial host context once connected
|
|
62
|
+
const initialContext = app?.getHostContext();
|
|
63
|
+
if (initialContext && !hostContext) {
|
|
64
|
+
setHostContext(initialContext);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Automatically apply host CSS variables, theme, and fonts
|
|
68
|
+
useHostStyles(app, initialContext);
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<MCPAppContext.Provider
|
|
72
|
+
value={{ app, isConnected, error, hostContext }}
|
|
73
|
+
>
|
|
74
|
+
{children}
|
|
75
|
+
</MCPAppContext.Provider>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Access the full MCP App context including the app instance and host context.
|
|
81
|
+
*
|
|
82
|
+
* Must be called within an `MCPAppProvider` (which is set up automatically
|
|
83
|
+
* by `renderMCPApp`).
|
|
84
|
+
*
|
|
85
|
+
* @returns The context value with app, connection state, and host context
|
|
86
|
+
* @throws If called outside of an MCPAppProvider
|
|
87
|
+
*
|
|
88
|
+
* @internal Used by `useMCPAppProps` and `useMCPApp`.
|
|
89
|
+
*/
|
|
90
|
+
export function useMCPAppContext(): MCPAppContextValue {
|
|
91
|
+
const ctx = useContext(MCPAppContext);
|
|
92
|
+
if (!ctx) {
|
|
93
|
+
throw new Error(
|
|
94
|
+
'useMCPAppContext must be used within an MCPAppProvider. ' +
|
|
95
|
+
'If you are using renderMCPApp(), this is set up automatically. ' +
|
|
96
|
+
'Otherwise, wrap your component tree with <MCPAppProvider>.'
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
return ctx;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Get the current host context from the MCP host application.
|
|
104
|
+
*
|
|
105
|
+
* Returns the full `McpUiHostContext` from the host (e.g., Claude Desktop),
|
|
106
|
+
* which includes theme, styles, display mode, locale, container dimensions,
|
|
107
|
+
* and more. The value updates automatically when the host sends
|
|
108
|
+
* `host-context-changed` notifications.
|
|
109
|
+
*
|
|
110
|
+
* Must be called within an `MCPAppProvider` (set up automatically
|
|
111
|
+
* by `renderMCPApp`).
|
|
112
|
+
*
|
|
113
|
+
* @returns The current host context, or undefined if not yet connected
|
|
114
|
+
*
|
|
115
|
+
* @example Access host display mode
|
|
116
|
+
* ```tsx
|
|
117
|
+
* import { useMCPHostContext } from '@mcp-web/app';
|
|
118
|
+
*
|
|
119
|
+
* function MyApp() {
|
|
120
|
+
* const hostContext = useMCPHostContext();
|
|
121
|
+
*
|
|
122
|
+
* return (
|
|
123
|
+
* <div>
|
|
124
|
+
* <p>Display mode: {hostContext?.displayMode}</p>
|
|
125
|
+
* <p>Platform: {hostContext?.platform}</p>
|
|
126
|
+
* </div>
|
|
127
|
+
* );
|
|
128
|
+
* }
|
|
129
|
+
* ```
|
|
130
|
+
*/
|
|
131
|
+
export function useMCPHostContext(): McpUiHostContext | undefined {
|
|
132
|
+
return useMCPAppContext().hostContext;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Get the current theme preference from the MCP host application.
|
|
137
|
+
*
|
|
138
|
+
* Returns `"light"` or `"dark"` based on the host's current theme.
|
|
139
|
+
* Updates reactively when the user toggles theme in the host.
|
|
140
|
+
*
|
|
141
|
+
* This hook reads the theme from the `data-theme` attribute on
|
|
142
|
+
* `document.documentElement`, which is set automatically by the
|
|
143
|
+
* host styles system. It uses a `MutationObserver` internally so
|
|
144
|
+
* it will re-render your component whenever the theme changes.
|
|
145
|
+
*
|
|
146
|
+
* Must be called within an `MCPAppProvider` (set up automatically
|
|
147
|
+
* by `renderMCPApp`).
|
|
148
|
+
*
|
|
149
|
+
* @returns The current theme — `"light"` or `"dark"`
|
|
150
|
+
*
|
|
151
|
+
* @example Sync with Tailwind CSS dark mode
|
|
152
|
+
* ```tsx
|
|
153
|
+
* import { useMCPHostTheme } from '@mcp-web/app';
|
|
154
|
+
* import { useEffect } from 'react';
|
|
155
|
+
*
|
|
156
|
+
* function MyApp(props: MyProps) {
|
|
157
|
+
* const theme = useMCPHostTheme();
|
|
158
|
+
*
|
|
159
|
+
* useEffect(() => {
|
|
160
|
+
* document.documentElement.classList.toggle('dark', theme === 'dark');
|
|
161
|
+
* }, [theme]);
|
|
162
|
+
*
|
|
163
|
+
* return (
|
|
164
|
+
* <div className="bg-white dark:bg-gray-900">
|
|
165
|
+
* {props.children}
|
|
166
|
+
* </div>
|
|
167
|
+
* );
|
|
168
|
+
* }
|
|
169
|
+
* ```
|
|
170
|
+
*
|
|
171
|
+
* @example Conditional rendering based on theme
|
|
172
|
+
* ```tsx
|
|
173
|
+
* import { useMCPHostTheme } from '@mcp-web/app';
|
|
174
|
+
*
|
|
175
|
+
* function Logo() {
|
|
176
|
+
* const theme = useMCPHostTheme();
|
|
177
|
+
* return <img src={theme === 'dark' ? '/logo-light.svg' : '/logo-dark.svg'} />;
|
|
178
|
+
* }
|
|
179
|
+
* ```
|
|
180
|
+
*/
|
|
181
|
+
export function useMCPHostTheme() {
|
|
182
|
+
return useDocumentTheme();
|
|
183
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import type { ComponentType } from 'react';
|
|
2
|
+
import { StrictMode, Suspense } from 'react';
|
|
3
|
+
import { createRoot } from 'react-dom/client';
|
|
4
|
+
import { MCPAppProvider } from './mcp-app-context.js';
|
|
5
|
+
import { useMCPAppProps } from './use-mcp-app-props.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Options for rendering an MCP App.
|
|
9
|
+
*/
|
|
10
|
+
export interface RenderMCPAppOptions {
|
|
11
|
+
/**
|
|
12
|
+
* Component to show while waiting for props.
|
|
13
|
+
* @default A simple "Loading..." div
|
|
14
|
+
*/
|
|
15
|
+
loading?: ComponentType;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* ID of the root element to mount the app.
|
|
19
|
+
* @default 'root'
|
|
20
|
+
*/
|
|
21
|
+
rootId?: string;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Whether to wrap the app in React.StrictMode.
|
|
25
|
+
* @default true
|
|
26
|
+
*/
|
|
27
|
+
strictMode?: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Default loading component shown while waiting for props.
|
|
32
|
+
* Uses host CSS variable with fallback for theme-aware loading text.
|
|
33
|
+
*/
|
|
34
|
+
function DefaultLoading() {
|
|
35
|
+
return (
|
|
36
|
+
<div
|
|
37
|
+
style={{
|
|
38
|
+
display: 'flex',
|
|
39
|
+
alignItems: 'center',
|
|
40
|
+
justifyContent: 'center',
|
|
41
|
+
height: '100%',
|
|
42
|
+
fontFamily: 'var(--font-sans, system-ui, sans-serif)',
|
|
43
|
+
color: 'var(--color-text-secondary, #666)',
|
|
44
|
+
}}
|
|
45
|
+
>
|
|
46
|
+
Loading...
|
|
47
|
+
</div>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Internal wrapper that handles props subscription.
|
|
53
|
+
*/
|
|
54
|
+
function MCPAppWrapper<P extends Record<string, unknown>>({
|
|
55
|
+
Component,
|
|
56
|
+
Loading,
|
|
57
|
+
}: {
|
|
58
|
+
Component: ComponentType<P>;
|
|
59
|
+
Loading: ComponentType;
|
|
60
|
+
}) {
|
|
61
|
+
const props = useMCPAppProps<P>();
|
|
62
|
+
|
|
63
|
+
if (!props) {
|
|
64
|
+
return <Loading />;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return <Component {...props} />;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Render a React component as an MCP App.
|
|
72
|
+
*
|
|
73
|
+
* This helper sets up the React root and handles props subscription,
|
|
74
|
+
* so your component can be a regular React component that receives props -
|
|
75
|
+
* no MCP-specific code required in your component.
|
|
76
|
+
*
|
|
77
|
+
* @param Component - Your React component (receives props from the MCP handler)
|
|
78
|
+
* @param options - Optional configuration for loading state and root element
|
|
79
|
+
*
|
|
80
|
+
* @example Basic usage with an existing component
|
|
81
|
+
* ```tsx
|
|
82
|
+
* // src/apps/stats.tsx
|
|
83
|
+
* import { renderMCPApp } from '@mcp-web/app';
|
|
84
|
+
* import { Stats } from '../components/Stats';
|
|
85
|
+
*
|
|
86
|
+
* // Stats is a regular component: function Stats({ total, completed }: StatsProps) { ... }
|
|
87
|
+
* renderMCPApp(Stats);
|
|
88
|
+
* ```
|
|
89
|
+
*
|
|
90
|
+
* @example With custom loading component
|
|
91
|
+
* ```tsx
|
|
92
|
+
* import { renderMCPApp } from '@mcp-web/app';
|
|
93
|
+
* import { Stats } from '../components/Stats';
|
|
94
|
+
* import { Spinner } from '../components/Spinner';
|
|
95
|
+
*
|
|
96
|
+
* renderMCPApp(Stats, {
|
|
97
|
+
* loading: Spinner,
|
|
98
|
+
* });
|
|
99
|
+
* ```
|
|
100
|
+
*
|
|
101
|
+
* @example Inline component definition
|
|
102
|
+
* ```tsx
|
|
103
|
+
* import { renderMCPApp } from '@mcp-web/app';
|
|
104
|
+
*
|
|
105
|
+
* interface ChartProps {
|
|
106
|
+
* data: number[];
|
|
107
|
+
* title: string;
|
|
108
|
+
* }
|
|
109
|
+
*
|
|
110
|
+
* renderMCPApp<ChartProps>(({ data, title }) => (
|
|
111
|
+
* <div>
|
|
112
|
+
* <h1>{title}</h1>
|
|
113
|
+
* <Chart data={data} />
|
|
114
|
+
* </div>
|
|
115
|
+
* ));
|
|
116
|
+
* ```
|
|
117
|
+
*/
|
|
118
|
+
export function renderMCPApp<P extends Record<string, unknown>>(
|
|
119
|
+
Component: ComponentType<P>,
|
|
120
|
+
options: RenderMCPAppOptions = {}
|
|
121
|
+
): void {
|
|
122
|
+
const { loading: Loading = DefaultLoading, rootId = 'root', strictMode = true } = options;
|
|
123
|
+
|
|
124
|
+
const rootElement = document.getElementById(rootId);
|
|
125
|
+
if (!rootElement) {
|
|
126
|
+
throw new Error(
|
|
127
|
+
`[renderMCPApp] Could not find element with id "${rootId}". ` +
|
|
128
|
+
`Make sure your HTML has <div id="${rootId}"></div>`
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const app = (
|
|
133
|
+
<Suspense fallback={<Loading />}>
|
|
134
|
+
<MCPAppProvider>
|
|
135
|
+
<MCPAppWrapper Component={Component} Loading={Loading} />
|
|
136
|
+
</MCPAppProvider>
|
|
137
|
+
</Suspense>
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
createRoot(rootElement).render(strictMode ? <StrictMode>{app}</StrictMode> : app);
|
|
141
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
import { useMCPAppContext } from './mcp-app-context.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* React hook to receive props from the MCP host via the ext-apps protocol.
|
|
6
|
+
*
|
|
7
|
+
* This hook connects to the host (e.g., Claude Desktop) using the
|
|
8
|
+
* `@modelcontextprotocol/ext-apps` JSON-RPC protocol. It listens for
|
|
9
|
+
* `tool-result` notifications, which contain the props returned by the
|
|
10
|
+
* tool handler as JSON in `content[0].text`.
|
|
11
|
+
*
|
|
12
|
+
* Must be called within an `MCPAppProvider` (set up automatically
|
|
13
|
+
* by `renderMCPApp`).
|
|
14
|
+
*
|
|
15
|
+
* @template T - The type of props expected from the handler
|
|
16
|
+
* @returns The props object, or null if not yet received
|
|
17
|
+
*
|
|
18
|
+
* @example Basic Usage
|
|
19
|
+
* ```tsx
|
|
20
|
+
* import { useMCPAppProps } from '@mcp-web/app';
|
|
21
|
+
*
|
|
22
|
+
* interface MyAppProps {
|
|
23
|
+
* title: string;
|
|
24
|
+
* data: number[];
|
|
25
|
+
* }
|
|
26
|
+
*
|
|
27
|
+
* function MyApp() {
|
|
28
|
+
* const props = useMCPAppProps<MyAppProps>();
|
|
29
|
+
*
|
|
30
|
+
* if (!props) {
|
|
31
|
+
* return <div>Waiting for data...</div>;
|
|
32
|
+
* }
|
|
33
|
+
*
|
|
34
|
+
* return (
|
|
35
|
+
* <div>
|
|
36
|
+
* <h1>{props.title}</h1>
|
|
37
|
+
* <ul>
|
|
38
|
+
* {props.data.map((item, i) => (
|
|
39
|
+
* <li key={i}>{item}</li>
|
|
40
|
+
* ))}
|
|
41
|
+
* </ul>
|
|
42
|
+
* </div>
|
|
43
|
+
* );
|
|
44
|
+
* }
|
|
45
|
+
* ```
|
|
46
|
+
*
|
|
47
|
+
* @example With Default Props for Development
|
|
48
|
+
* ```tsx
|
|
49
|
+
* function MyApp() {
|
|
50
|
+
* const props = useMCPAppProps<MyAppProps>() ?? {
|
|
51
|
+
* title: 'Development Preview',
|
|
52
|
+
* data: [1, 2, 3],
|
|
53
|
+
* };
|
|
54
|
+
*
|
|
55
|
+
* return <div>{props.title}</div>;
|
|
56
|
+
* }
|
|
57
|
+
* ```
|
|
58
|
+
*/
|
|
59
|
+
export function useMCPAppProps<T>(): T | null {
|
|
60
|
+
const [props, setProps] = useState<T | null>(null);
|
|
61
|
+
const { app } = useMCPAppContext();
|
|
62
|
+
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
if (!app) return;
|
|
65
|
+
|
|
66
|
+
// Listen for tool result - this is where our props come from.
|
|
67
|
+
// The tool handler returns props which the bridge wraps into
|
|
68
|
+
// CallToolResult.content[0].text as JSON.
|
|
69
|
+
app.ontoolresult = async (result) => {
|
|
70
|
+
if (result?.content) {
|
|
71
|
+
for (const block of result.content) {
|
|
72
|
+
if (block.type === 'text' && typeof block.text === 'string') {
|
|
73
|
+
try {
|
|
74
|
+
const parsed = JSON.parse(block.text);
|
|
75
|
+
setProps(parsed as T);
|
|
76
|
+
} catch {
|
|
77
|
+
// If JSON parsing fails, treat the text as-is
|
|
78
|
+
setProps(block.text as unknown as T);
|
|
79
|
+
}
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
// Also listen for tool input - useful for apps that need
|
|
87
|
+
// the raw tool call arguments
|
|
88
|
+
app.ontoolinput = async (input) => {
|
|
89
|
+
// If we have arguments and no result yet, use them as initial props.
|
|
90
|
+
// This enables apps to render immediately with the tool input
|
|
91
|
+
// before the full result arrives.
|
|
92
|
+
if (input?.arguments) {
|
|
93
|
+
setProps((prev) => prev ?? (input.arguments as unknown as T));
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
}, [app]);
|
|
97
|
+
|
|
98
|
+
return props;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Get the ext-apps `App` instance for advanced use cases.
|
|
103
|
+
*
|
|
104
|
+
* This hook provides access to the underlying `App` class from
|
|
105
|
+
* `@modelcontextprotocol/ext-apps`, enabling bidirectional communication
|
|
106
|
+
* with the host (e.g., calling server tools, sending messages).
|
|
107
|
+
*
|
|
108
|
+
* Must be called within an `MCPAppProvider` (set up automatically
|
|
109
|
+
* by `renderMCPApp`).
|
|
110
|
+
*
|
|
111
|
+
* @returns The App state including app instance, connection status, and errors
|
|
112
|
+
*
|
|
113
|
+
* @example Calling a server tool from the app
|
|
114
|
+
* ```tsx
|
|
115
|
+
* import { useMCPApp } from '@mcp-web/app';
|
|
116
|
+
*
|
|
117
|
+
* function MyApp() {
|
|
118
|
+
* const { app, isConnected } = useMCPApp();
|
|
119
|
+
*
|
|
120
|
+
* const handleClick = async () => {
|
|
121
|
+
* if (app) {
|
|
122
|
+
* const result = await app.callServerTool({
|
|
123
|
+
* name: 'update_data',
|
|
124
|
+
* arguments: { key: 'value' },
|
|
125
|
+
* });
|
|
126
|
+
* console.log('Server tool result:', result);
|
|
127
|
+
* }
|
|
128
|
+
* };
|
|
129
|
+
*
|
|
130
|
+
* return <button onClick={handleClick}>Update</button>;
|
|
131
|
+
* }
|
|
132
|
+
* ```
|
|
133
|
+
*/
|
|
134
|
+
export function useMCPApp() {
|
|
135
|
+
const { app, isConnected, error } = useMCPAppContext();
|
|
136
|
+
return { app, isConnected, error };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Get current MCP App props synchronously.
|
|
141
|
+
*
|
|
142
|
+
* @deprecated Use `useMCPAppProps` hook instead. This function is maintained
|
|
143
|
+
* for backward compatibility but does not work with the ext-apps protocol.
|
|
144
|
+
*
|
|
145
|
+
* @template T - The type of props expected
|
|
146
|
+
* @returns null (ext-apps protocol is async-only)
|
|
147
|
+
*/
|
|
148
|
+
export function getMCPAppProps<T>(): T | null {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Subscribe to MCP App props changes.
|
|
154
|
+
*
|
|
155
|
+
* @deprecated Use `useMCPAppProps` hook instead. This function is maintained
|
|
156
|
+
* for backward compatibility but does not work with the ext-apps protocol.
|
|
157
|
+
*
|
|
158
|
+
* @template T - The type of props expected
|
|
159
|
+
* @param _listener - Function called when props are received or updated
|
|
160
|
+
* @returns No-op unsubscribe function
|
|
161
|
+
*/
|
|
162
|
+
export function subscribeMCPAppProps<T>(
|
|
163
|
+
_listener: (props: T) => void
|
|
164
|
+
): () => void {
|
|
165
|
+
return () => {};
|
|
166
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { defineMCPAppsConfig } from '../src/vite-plugin';
|
|
3
|
+
import type { UserConfig } from 'vite';
|
|
4
|
+
|
|
5
|
+
describe('defineMCPAppsConfig', () => {
|
|
6
|
+
test('returns a Vite config object', () => {
|
|
7
|
+
const config = defineMCPAppsConfig() as UserConfig;
|
|
8
|
+
|
|
9
|
+
expect(config).toBeDefined();
|
|
10
|
+
expect(config.base).toBe('./');
|
|
11
|
+
expect(config.build).toBeDefined();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test('sets critical build options for single-file output', () => {
|
|
15
|
+
const config = defineMCPAppsConfig() as UserConfig;
|
|
16
|
+
|
|
17
|
+
expect(config.build?.assetsInlineLimit).toBe(Number.MAX_SAFE_INTEGER);
|
|
18
|
+
expect(config.build?.cssCodeSplit).toBe(false);
|
|
19
|
+
expect(config.build?.emptyOutDir).toBe(true);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test('includes viteSingleFile and mcpAppPlugin plugins', () => {
|
|
23
|
+
const config = defineMCPAppsConfig() as UserConfig;
|
|
24
|
+
|
|
25
|
+
expect(config.plugins).toBeDefined();
|
|
26
|
+
expect(Array.isArray(config.plugins)).toBe(true);
|
|
27
|
+
|
|
28
|
+
const pluginNames = (config.plugins as Array<{ name?: string }>)
|
|
29
|
+
.filter((p) => p && typeof p === 'object' && 'name' in p)
|
|
30
|
+
.map((p) => p.name);
|
|
31
|
+
|
|
32
|
+
expect(pluginNames).toContain('vite:singlefile');
|
|
33
|
+
expect(pluginNames).toContain('mcp-web-app');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('merges user plugins with internal plugins', () => {
|
|
37
|
+
const userPlugin = { name: 'user-plugin' };
|
|
38
|
+
const config = defineMCPAppsConfig({
|
|
39
|
+
plugins: [userPlugin],
|
|
40
|
+
}) as UserConfig;
|
|
41
|
+
|
|
42
|
+
const pluginNames = (config.plugins as Array<{ name?: string }>)
|
|
43
|
+
.filter((p) => p && typeof p === 'object' && 'name' in p)
|
|
44
|
+
.map((p) => p.name);
|
|
45
|
+
|
|
46
|
+
expect(pluginNames).toContain('user-plugin');
|
|
47
|
+
expect(pluginNames).toContain('vite:singlefile');
|
|
48
|
+
expect(pluginNames).toContain('mcp-web-app');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('user plugins come before internal plugins', () => {
|
|
52
|
+
const userPlugin = { name: 'user-plugin' };
|
|
53
|
+
const config = defineMCPAppsConfig({
|
|
54
|
+
plugins: [userPlugin],
|
|
55
|
+
}) as UserConfig;
|
|
56
|
+
|
|
57
|
+
const pluginNames = (config.plugins as Array<{ name?: string }>)
|
|
58
|
+
.filter((p) => p && typeof p === 'object' && 'name' in p)
|
|
59
|
+
.map((p) => p.name);
|
|
60
|
+
|
|
61
|
+
const userIndex = pluginNames.indexOf('user-plugin');
|
|
62
|
+
const singleFileIndex = pluginNames.indexOf('vite:singlefile');
|
|
63
|
+
|
|
64
|
+
expect(userIndex).toBeLessThan(singleFileIndex);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('accepts custom appsConfig and outDir', () => {
|
|
68
|
+
const config = defineMCPAppsConfig(
|
|
69
|
+
{},
|
|
70
|
+
{
|
|
71
|
+
appsConfig: 'custom/apps.ts',
|
|
72
|
+
outDir: 'custom/output',
|
|
73
|
+
}
|
|
74
|
+
) as UserConfig;
|
|
75
|
+
|
|
76
|
+
expect(config).toBeDefined();
|
|
77
|
+
expect(config.build?.outDir).toContain('custom/output');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('preserves user build options that are not critical', () => {
|
|
81
|
+
const config = defineMCPAppsConfig({
|
|
82
|
+
build: {
|
|
83
|
+
sourcemap: true,
|
|
84
|
+
minify: 'terser',
|
|
85
|
+
},
|
|
86
|
+
}) as UserConfig;
|
|
87
|
+
|
|
88
|
+
expect(config.build?.sourcemap).toBe(true);
|
|
89
|
+
expect(config.build?.minify).toBe('terser');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('overrides critical settings even if user provides them', () => {
|
|
93
|
+
const config = defineMCPAppsConfig({
|
|
94
|
+
build: {
|
|
95
|
+
assetsInlineLimit: 100,
|
|
96
|
+
cssCodeSplit: true,
|
|
97
|
+
},
|
|
98
|
+
}) as UserConfig;
|
|
99
|
+
|
|
100
|
+
// Critical settings should be overridden
|
|
101
|
+
expect(config.build?.assetsInlineLimit).toBe(Number.MAX_SAFE_INTEGER);
|
|
102
|
+
expect(config.build?.cssCodeSplit).toBe(false);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test('default export is same as named export', async () => {
|
|
106
|
+
const { default: defaultExport, defineMCPAppsConfig: namedExport } =
|
|
107
|
+
await import('../src/vite-plugin');
|
|
108
|
+
expect(defaultExport).toBe(namedExport);
|
|
109
|
+
});
|
|
110
|
+
});
|