mcp-app-studio 0.3.2

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/README.md ADDED
@@ -0,0 +1,152 @@
1
+ # MCP App Studio
2
+
3
+ **Build interactive apps for AI assistants (ChatGPT, Claude, MCP hosts).**
4
+
5
+ Create widgets that work across multiple platforms with a single codebase. The SDK auto-detects whether you're running in ChatGPT, Claude Desktop, or another MCP-compatible host.
6
+
7
+ ## What You Get
8
+
9
+ - **Local workbench** — Preview widgets without deploying
10
+ - **Universal SDK** — Single API works on ChatGPT and MCP hosts
11
+ - **Platform detection** — Auto-adapts to the host environment
12
+ - **One-command export** — Generate production bundle + MCP server
13
+
14
+ ## Quick Start
15
+
16
+ ```bash
17
+ npx mcp-app-studio my-app
18
+ cd my-app
19
+ npm install
20
+ npm run dev
21
+ ```
22
+
23
+ Open http://localhost:3000 — you're in the workbench.
24
+
25
+ ## Universal SDK
26
+
27
+ The SDK provides React hooks that work identically across platforms:
28
+
29
+ ```tsx
30
+ import {
31
+ UniversalProvider,
32
+ usePlatform,
33
+ useToolInput,
34
+ useCallTool,
35
+ useTheme,
36
+ useFeature
37
+ } from "mcp-app-studio";
38
+
39
+ function MyWidget() {
40
+ const platform = usePlatform(); // "chatgpt" | "mcp" | "unknown"
41
+ const input = useToolInput<{ query: string }>();
42
+ const callTool = useCallTool();
43
+ const theme = useTheme();
44
+
45
+ // Platform-specific features
46
+ const hasWidgetState = useFeature('widgetState'); // ChatGPT only
47
+ const hasModelContext = useFeature('modelContext'); // MCP only
48
+
49
+ return (
50
+ <div className={theme === 'dark' ? 'bg-gray-900' : 'bg-white'}>
51
+ {/* Your widget */}
52
+ </div>
53
+ );
54
+ }
55
+
56
+ // Wrap your app
57
+ function App() {
58
+ return (
59
+ <UniversalProvider>
60
+ <MyWidget />
61
+ </UniversalProvider>
62
+ );
63
+ }
64
+ ```
65
+
66
+ ## Platform Capabilities
67
+
68
+ | Feature | ChatGPT | MCP |
69
+ |---------|---------|-----|
70
+ | `callTool` | ✅ | ✅ |
71
+ | `openLink` | ✅ | ✅ |
72
+ | `sendMessage` | ✅ | ✅ |
73
+ | `widgetState` (persistence) | ✅ | ❌ |
74
+ | `modelContext` (dynamic context) | ❌ | ✅ |
75
+ | `fileUpload` / `fileDownload` | ✅ | ❌ |
76
+ | `partialToolInput` (streaming) | ❌ | ✅ |
77
+
78
+ ## Workflow
79
+
80
+ ```
81
+ 1. DEVELOP npm run dev Edit widgets, test with mock tools
82
+ 2. EXPORT npm run export Generate widget bundle + manifest
83
+ 3. DEPLOY Your choice Vercel, Netlify, any static host
84
+ 4. REGISTER Platform dashboard Connect your app
85
+ ```
86
+
87
+ ## Generated Project
88
+
89
+ ```
90
+ my-app/
91
+ ├── app/ Next.js app
92
+ ├── components/
93
+ │ └── examples/ Example widgets
94
+ ├── lib/
95
+ │ ├── workbench/ Dev environment + React hooks
96
+ │ └── export/ Production bundler
97
+ └── server/ MCP server (if selected)
98
+ ```
99
+
100
+ ## Export Output
101
+
102
+ ```bash
103
+ npm run export
104
+ ```
105
+
106
+ Generates:
107
+
108
+ ```
109
+ export/
110
+ ├── widget/
111
+ │ └── index.html Self-contained widget (deploy to static host)
112
+ ├── manifest.json App manifest
113
+ └── README.md Deployment instructions
114
+ ```
115
+
116
+ ## Debugging
117
+
118
+ Enable debug mode to troubleshoot platform detection:
119
+
120
+ ```ts
121
+ import { enableDebugMode, detectPlatformDetailed } from "mcp-app-studio";
122
+
123
+ // In browser console or before app init
124
+ enableDebugMode();
125
+
126
+ // Get detailed detection info
127
+ const result = detectPlatformDetailed();
128
+ console.log('Platform:', result.platform);
129
+ console.log('Detected by:', result.detectedBy);
130
+ console.log('Checks:', result.checks);
131
+ ```
132
+
133
+ ## MCP Server
134
+
135
+ If you selected "Include MCP server" during setup:
136
+
137
+ ```bash
138
+ cd server
139
+ npm install
140
+ npm run dev # http://localhost:3001/mcp
141
+ npm run inspect # Test with MCP Inspector
142
+ ```
143
+
144
+ ## Learn More
145
+
146
+ - [MCP Apps Specification](https://modelcontextprotocol.io/specification/)
147
+ - [ChatGPT Apps SDK](https://developers.openai.com/apps-sdk/)
148
+ - [assistant-ui](https://www.assistant-ui.com/)
149
+
150
+ ## License
151
+
152
+ MIT
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ import("../dist/cli/index.js").catch((err) => {
3
+ console.error(err);
4
+ process.exit(1);
5
+ });
@@ -0,0 +1,66 @@
1
+ import { McpUiHostCapabilities, App } from '@modelcontextprotocol/ext-apps';
2
+ import { DisplayMode, ExtendedBridge, HostCapabilities, HostContext, ToolInputCallback, ToolInputPartialCallback, ToolResultCallback, ToolCancelledCallback, HostContextChangedCallback, TeardownCallback, ToolResult, ChatMessage, ContentBlock } from './core/index.js';
3
+
4
+ interface AppCapabilities {
5
+ tools?: {
6
+ listChanged?: boolean;
7
+ };
8
+ resources?: {
9
+ listChanged?: boolean;
10
+ };
11
+ prompts?: {
12
+ listChanged?: boolean;
13
+ };
14
+ displayMode?: {
15
+ supported: DisplayMode[];
16
+ };
17
+ }
18
+ interface MCPBridgeOptions {
19
+ autoResize?: boolean;
20
+ }
21
+ type CallToolHandler = (name: string, args: Record<string, unknown>, extra: unknown) => Promise<ToolResult>;
22
+ type ListToolsHandler = (cursor?: string) => Promise<string[]>;
23
+ declare class MCPBridge implements ExtendedBridge {
24
+ readonly platform: "mcp";
25
+ readonly capabilities: HostCapabilities;
26
+ private app;
27
+ private toolInputCallbacks;
28
+ private toolInputPartialCallbacks;
29
+ private toolResultCallbacks;
30
+ private toolCancelledCallbacks;
31
+ private contextCallbacks;
32
+ private teardownCallbacks;
33
+ constructor(appInfo?: {
34
+ name: string;
35
+ version: string;
36
+ }, appCapabilities?: AppCapabilities, options?: MCPBridgeOptions);
37
+ connect(): Promise<void>;
38
+ getHostContext(): HostContext | null;
39
+ private mapHostContext;
40
+ onToolInput(callback: ToolInputCallback): () => void;
41
+ onToolInputPartial(callback: ToolInputPartialCallback): () => void;
42
+ onToolResult(callback: ToolResultCallback): () => void;
43
+ onToolCancelled(callback: ToolCancelledCallback): () => void;
44
+ onHostContextChanged(callback: HostContextChangedCallback): () => void;
45
+ onTeardown(callback: TeardownCallback): () => void;
46
+ callTool(name: string, args: Record<string, unknown>): Promise<ToolResult>;
47
+ openLink(url: string): Promise<void>;
48
+ requestDisplayMode(mode: DisplayMode): Promise<DisplayMode>;
49
+ sendSizeChanged(size: {
50
+ width?: number;
51
+ height?: number;
52
+ }): void;
53
+ sendMessage(message: ChatMessage): Promise<void>;
54
+ updateModelContext(ctx: {
55
+ content?: ContentBlock[];
56
+ structuredContent?: Record<string, unknown>;
57
+ }): Promise<void>;
58
+ sendLog(level: "debug" | "info" | "notice" | "warning" | "error" | "critical" | "alert" | "emergency", data: string, logger?: string): void;
59
+ setCallToolHandler(handler: CallToolHandler): void;
60
+ setListToolsHandler(handler: ListToolsHandler): void;
61
+ getHostCapabilities(): McpUiHostCapabilities | undefined;
62
+ setupSizeChangedNotifications(): () => void;
63
+ getApp(): App;
64
+ }
65
+
66
+ export { type AppCapabilities as A, MCPBridge as M, type MCPBridgeOptions as a };
@@ -0,0 +1,47 @@
1
+ // src/core/capabilities.ts
2
+ var CHATGPT_CAPABILITIES = {
3
+ platform: "chatgpt",
4
+ callTool: true,
5
+ openLink: true,
6
+ displayModes: ["pip", "inline", "fullscreen"],
7
+ sizeReporting: true,
8
+ closeWidget: true,
9
+ sendMessage: true,
10
+ modal: true,
11
+ fileUpload: true,
12
+ fileDownload: true,
13
+ widgetState: true,
14
+ modelContext: false,
15
+ logging: false,
16
+ partialToolInput: false,
17
+ toolCancellation: false,
18
+ teardown: false
19
+ };
20
+ var MCP_CAPABILITIES = {
21
+ platform: "mcp",
22
+ callTool: true,
23
+ openLink: true,
24
+ displayModes: ["inline", "fullscreen", "pip"],
25
+ sizeReporting: true,
26
+ closeWidget: false,
27
+ sendMessage: true,
28
+ modal: false,
29
+ fileUpload: false,
30
+ fileDownload: false,
31
+ widgetState: false,
32
+ modelContext: true,
33
+ logging: true,
34
+ partialToolInput: true,
35
+ toolCancellation: true,
36
+ teardown: true
37
+ };
38
+ function hasFeature(capabilities, feature) {
39
+ if (!capabilities) return false;
40
+ return capabilities[feature] === true;
41
+ }
42
+
43
+ export {
44
+ CHATGPT_CAPABILITIES,
45
+ MCP_CAPABILITIES,
46
+ hasFeature
47
+ };
@@ -0,0 +1,16 @@
1
+ // src/core/types.ts
2
+ function textBlock(text, annotations) {
3
+ const block = { type: "text", text };
4
+ if (annotations) block.annotations = annotations;
5
+ return block;
6
+ }
7
+ function imageBlock(data, mimeType, annotations) {
8
+ const block = { type: "image", data, mimeType };
9
+ if (annotations) block.annotations = annotations;
10
+ return block;
11
+ }
12
+
13
+ export {
14
+ textBlock,
15
+ imageBlock
16
+ };
@@ -0,0 +1,140 @@
1
+ import {
2
+ CHATGPT_CAPABILITIES
3
+ } from "./chunk-4LAH4JH6.js";
4
+
5
+ // src/platforms/chatgpt/bridge.ts
6
+ var ChatGPTBridge = class {
7
+ platform = "chatgpt";
8
+ capabilities = CHATGPT_CAPABILITIES;
9
+ toolInputCallbacks = /* @__PURE__ */ new Set();
10
+ toolResultCallbacks = /* @__PURE__ */ new Set();
11
+ contextCallbacks = /* @__PURE__ */ new Set();
12
+ lastContext = null;
13
+ connected = false;
14
+ get openai() {
15
+ if (!window.openai) {
16
+ throw new Error("ChatGPT bridge not available");
17
+ }
18
+ return window.openai;
19
+ }
20
+ async connect() {
21
+ if (!window.openai) {
22
+ throw new Error(
23
+ "ChatGPT bridge not available. Is this running inside ChatGPT?"
24
+ );
25
+ }
26
+ window.addEventListener("openai:set_globals", this.handleGlobalsChange);
27
+ this.lastContext = this.buildHostContext();
28
+ if (this.openai.toolInput) {
29
+ this.toolInputCallbacks.forEach((cb) => cb(this.openai.toolInput));
30
+ }
31
+ if (this.openai.toolOutput) {
32
+ this.toolResultCallbacks.forEach(
33
+ (cb) => cb({
34
+ structuredContent: this.openai.toolOutput
35
+ })
36
+ );
37
+ }
38
+ this.connected = true;
39
+ }
40
+ buildHostContext() {
41
+ const g = this.openai;
42
+ return {
43
+ theme: g.theme,
44
+ locale: g.locale,
45
+ displayMode: g.displayMode,
46
+ availableDisplayModes: ["pip", "inline", "fullscreen"],
47
+ containerDimensions: { maxHeight: g.maxHeight },
48
+ platform: this.mapDeviceType(g.userAgent?.device?.type),
49
+ deviceCapabilities: g.userAgent?.capabilities,
50
+ safeAreaInsets: g.safeArea?.insets,
51
+ userAgent: "ChatGPT"
52
+ };
53
+ }
54
+ mapDeviceType(type) {
55
+ if (type === "mobile" || type === "tablet") return "mobile";
56
+ return "web";
57
+ }
58
+ handleGlobalsChange = () => {
59
+ const newContext = this.buildHostContext();
60
+ if (JSON.stringify(newContext) !== JSON.stringify(this.lastContext)) {
61
+ this.lastContext = newContext;
62
+ this.contextCallbacks.forEach((cb) => cb(newContext));
63
+ }
64
+ if (this.openai.toolInput) {
65
+ this.toolInputCallbacks.forEach((cb) => cb(this.openai.toolInput));
66
+ }
67
+ if (this.openai.toolOutput) {
68
+ const result = {
69
+ structuredContent: this.openai.toolOutput
70
+ };
71
+ if (this.openai.toolResponseMetadata) {
72
+ result._meta = this.openai.toolResponseMetadata;
73
+ }
74
+ this.toolResultCallbacks.forEach((cb) => cb(result));
75
+ }
76
+ };
77
+ getHostContext() {
78
+ return this.lastContext;
79
+ }
80
+ onToolInput(callback) {
81
+ this.toolInputCallbacks.add(callback);
82
+ if (this.connected && this.openai.toolInput) {
83
+ callback(this.openai.toolInput);
84
+ }
85
+ return () => this.toolInputCallbacks.delete(callback);
86
+ }
87
+ onToolResult(callback) {
88
+ this.toolResultCallbacks.add(callback);
89
+ if (this.connected && this.openai.toolOutput) {
90
+ callback({ structuredContent: this.openai.toolOutput });
91
+ }
92
+ return () => this.toolResultCallbacks.delete(callback);
93
+ }
94
+ onHostContextChanged(callback) {
95
+ this.contextCallbacks.add(callback);
96
+ return () => this.contextCallbacks.delete(callback);
97
+ }
98
+ async callTool(name, args) {
99
+ const result = await this.openai.callTool(name, args);
100
+ return { structuredContent: result };
101
+ }
102
+ async openLink(url) {
103
+ this.openai.openExternal({ href: url });
104
+ }
105
+ async requestDisplayMode(mode) {
106
+ const result = await this.openai.requestDisplayMode({ mode });
107
+ return result.mode;
108
+ }
109
+ sendSizeChanged(size) {
110
+ if (size.height != null) {
111
+ this.openai.notifyIntrinsicHeight(size.height);
112
+ }
113
+ }
114
+ async sendMessage(message) {
115
+ const text = message.content.filter((c) => c.type === "text").map((c) => c.text).join("\n");
116
+ await this.openai.sendFollowUpMessage({ prompt: text });
117
+ }
118
+ setWidgetState(state) {
119
+ this.openai.setWidgetState(state);
120
+ }
121
+ getWidgetState() {
122
+ return this.openai.widgetState;
123
+ }
124
+ async uploadFile(file) {
125
+ return this.openai.uploadFile(file);
126
+ }
127
+ async getFileDownloadUrl(fileId) {
128
+ return this.openai.getFileDownloadUrl({ fileId });
129
+ }
130
+ requestClose() {
131
+ this.openai.requestClose();
132
+ }
133
+ async requestModal(options) {
134
+ await this.openai.requestModal(options);
135
+ }
136
+ };
137
+
138
+ export {
139
+ ChatGPTBridge
140
+ };
@@ -0,0 +1,174 @@
1
+ import {
2
+ MCP_CAPABILITIES
3
+ } from "./chunk-4LAH4JH6.js";
4
+
5
+ // src/platforms/mcp/bridge.ts
6
+ import { App } from "@modelcontextprotocol/ext-apps";
7
+ var MCPBridge = class {
8
+ platform = "mcp";
9
+ capabilities = MCP_CAPABILITIES;
10
+ app;
11
+ toolInputCallbacks = /* @__PURE__ */ new Set();
12
+ toolInputPartialCallbacks = /* @__PURE__ */ new Set();
13
+ toolResultCallbacks = /* @__PURE__ */ new Set();
14
+ toolCancelledCallbacks = /* @__PURE__ */ new Set();
15
+ contextCallbacks = /* @__PURE__ */ new Set();
16
+ teardownCallbacks = /* @__PURE__ */ new Set();
17
+ constructor(appInfo, appCapabilities, options) {
18
+ const autoResize = options?.autoResize ?? true;
19
+ this.app = new App(
20
+ appInfo ?? { name: "MCP App", version: "1.0.0" },
21
+ appCapabilities ?? {},
22
+ { autoResize }
23
+ );
24
+ this.app.ontoolinput = (params) => {
25
+ this.toolInputCallbacks.forEach(
26
+ (cb) => cb(params.arguments)
27
+ );
28
+ };
29
+ this.app.ontoolinputpartial = (params) => {
30
+ this.toolInputPartialCallbacks.forEach(
31
+ (cb) => cb(params.arguments)
32
+ );
33
+ };
34
+ this.app.ontoolresult = (params) => {
35
+ const result = {
36
+ content: params.content,
37
+ structuredContent: params.structuredContent
38
+ };
39
+ if (params.isError !== void 0) {
40
+ result.isError = params.isError;
41
+ }
42
+ if (params._meta) {
43
+ result._meta = params._meta;
44
+ }
45
+ this.toolResultCallbacks.forEach((cb) => cb(result));
46
+ };
47
+ this.app.ontoolcancelled = (params) => {
48
+ this.toolCancelledCallbacks.forEach((cb) => cb(params.reason));
49
+ };
50
+ this.app.onhostcontextchanged = (params) => {
51
+ const ctx = this.mapHostContext(params);
52
+ this.contextCallbacks.forEach((cb) => cb(ctx));
53
+ };
54
+ this.app.onteardown = async () => {
55
+ for (const cb of this.teardownCallbacks) {
56
+ await cb();
57
+ }
58
+ return {};
59
+ };
60
+ }
61
+ async connect() {
62
+ await this.app.connect();
63
+ }
64
+ getHostContext() {
65
+ const ctx = this.app.getHostContext();
66
+ return ctx ? this.mapHostContext(ctx) : null;
67
+ }
68
+ mapHostContext(ctx) {
69
+ return ctx;
70
+ }
71
+ onToolInput(callback) {
72
+ this.toolInputCallbacks.add(callback);
73
+ return () => this.toolInputCallbacks.delete(callback);
74
+ }
75
+ onToolInputPartial(callback) {
76
+ this.toolInputPartialCallbacks.add(callback);
77
+ return () => this.toolInputPartialCallbacks.delete(callback);
78
+ }
79
+ onToolResult(callback) {
80
+ this.toolResultCallbacks.add(callback);
81
+ return () => this.toolResultCallbacks.delete(callback);
82
+ }
83
+ onToolCancelled(callback) {
84
+ this.toolCancelledCallbacks.add(callback);
85
+ return () => this.toolCancelledCallbacks.delete(callback);
86
+ }
87
+ onHostContextChanged(callback) {
88
+ this.contextCallbacks.add(callback);
89
+ return () => this.contextCallbacks.delete(callback);
90
+ }
91
+ onTeardown(callback) {
92
+ this.teardownCallbacks.add(callback);
93
+ return () => this.teardownCallbacks.delete(callback);
94
+ }
95
+ async callTool(name, args) {
96
+ const result = await this.app.callServerTool({ name, arguments: args });
97
+ const toolResult = {
98
+ content: result.content,
99
+ structuredContent: result.structuredContent
100
+ };
101
+ if (result.isError !== void 0) {
102
+ toolResult.isError = result.isError;
103
+ }
104
+ return toolResult;
105
+ }
106
+ async openLink(url) {
107
+ await this.app.openLink({ url });
108
+ }
109
+ async requestDisplayMode(mode) {
110
+ const result = await this.app.requestDisplayMode({ mode });
111
+ return result.mode;
112
+ }
113
+ sendSizeChanged(size) {
114
+ this.app.sendSizeChanged(size);
115
+ }
116
+ async sendMessage(message) {
117
+ await this.app.sendMessage({
118
+ role: message.role,
119
+ content: message.content.map((c) => {
120
+ if (c.type === "text") {
121
+ return { type: "text", text: c.text };
122
+ }
123
+ return c;
124
+ })
125
+ });
126
+ }
127
+ async updateModelContext(ctx) {
128
+ await this.app.updateModelContext(ctx);
129
+ }
130
+ sendLog(level, data, logger) {
131
+ this.app.sendLog({ level, data, logger });
132
+ }
133
+ setCallToolHandler(handler) {
134
+ this.app.oncalltool = async (params, extra) => {
135
+ const result = await handler(params.name, params.arguments ?? {}, extra);
136
+ const content = result.content?.map((c) => {
137
+ if (c.type === "text") {
138
+ return { type: "text", text: c.text };
139
+ }
140
+ if (c.type === "image" || c.type === "audio") {
141
+ return { type: c.type, data: c.data, mimeType: c.mimeType };
142
+ }
143
+ return c;
144
+ }) ?? [];
145
+ const response = { content };
146
+ if (result.structuredContent !== void 0) {
147
+ response.structuredContent = result.structuredContent;
148
+ }
149
+ if (result.isError !== void 0) {
150
+ response.isError = result.isError;
151
+ }
152
+ return response;
153
+ };
154
+ }
155
+ setListToolsHandler(handler) {
156
+ this.app.onlisttools = async (params) => {
157
+ const tools = await handler(params?.cursor);
158
+ return { tools };
159
+ };
160
+ }
161
+ getHostCapabilities() {
162
+ return this.app.getHostCapabilities();
163
+ }
164
+ setupSizeChangedNotifications() {
165
+ return this.app.setupSizeChangedNotifications();
166
+ }
167
+ getApp() {
168
+ return this.app;
169
+ }
170
+ };
171
+
172
+ export {
173
+ MCPBridge
174
+ };
@@ -0,0 +1,2 @@
1
+
2
+ export { }