BotDojo LogoInteractive Agent SDKBeta
About BotDojoBotDojo

Inline vs Remote MCP Apps

MCP Apps can be delivered as inline HTML (bundled string) or remote URL (hosted content). Both have identical capabilitiesβ€”the only difference is how the HTML is delivered.

Important: Both inline and remote MCP Apps have identical capabilities including: sendMessage(), callTool(), openLink(), reportSize(), fetch() for external APIs, loading external images, streaming updates, and state persistence.

Two Ways to Deliver Your MCP App

πŸ“¦

Inline HTML

Your React component is compiled to an HTML string and provided through the MCP resources array. The HTML is served through a proxy and runs in a sandboxed iframe. Note: Inline HTML is delivered via the proxy on each load and won't be browser-cached like traditional static files.

🌐

Remote URL (HTTPS Required)

Your MCP App is hosted at an HTTPS URL (CDN, server, etc.). The host fetches and serves it through a proxy to the sandboxed iframe. Remote URLs benefit from browser caching and can be deployed independently.

Inline HTML

How Bundling Works

In the SDK Playground, we use Next.js to bundle React components into standalone HTML:

  1. Create a React component in pages/examples/mcp-apps/widgets/my-app.tsx
  2. Next.js compiles it to a standalone HTML page with all dependencies
  3. Use fetchMcpAppHtml('my-app') to read the compiled HTML
  4. Return the HTML string in your resource's getContent()
πŸ“„Example: Inline HTML Resourcetypescript
1resources: [
2 {
3 uri: 'ui://my-app/widget',
4 name: 'My Widget',
5 mimeType: 'text/html;profile=mcp-app',
6 getContent: async () => {
7 // In the playground, we use Next.js bundling:
8 const { fetchMcpAppHtml } = await import('@/utils/fetchMcpApp');
9 const html = await fetchMcpAppHtml('my-widget-app');
10
11 return {
12 uri: 'ui://my-app/widget',
13 mimeType: 'text/html;profile=mcp-app',
14 text: html // <-- HTML as a string
15 };
16 }
17 }
18]

Remote URL (HTTPS Required)

Host your MCP App on any HTTPS server (CDN, Vercel, Netlify, your own server, etc.). Remote URLs must use HTTPS because MCP Apps are served through a secure proxy.

Local Development with ngrok

For local development in this playground, you can use ngrok to create an HTTPS tunnel:

πŸ“„Create HTTPS Tunnelbash
1# In this playground, run:
2npm run dev:ngrok
3
4# This creates an HTTPS tunnel to localhost:3500
5# Use the provided https://xxx.ngrok.io URL as your resourceUri

Define Your Tool with _meta

The _meta field is defined in the MCP specification (SEP-1865) and allows you to attach UI resources to tools:

πŸ“„Example: Remote URL Tooltypescript
1{
2 name: 'show_widget',
3 description: 'Display widget',
4 inputSchema: { /* ... */ },
5 _meta: { // From MCP spec (SEP-1865)
6 ui: {
7 resourceUri: 'https://mycdn.com/widgets/product-card',
8 csp: {
9 resourceDomains: ['https://mycdn.com'],
10 connectDomains: ['https://api.myapp.com']
11 }
12 }
13 },
14 execute: async (args) => {
15 return [textResult('Widget displayed')];
16 }
17}

CORS & External Resources

Both inline and remote MCP Apps can access external resources, but you must configure CSP (Content Security Policy) in your tool metadata.

For External Images, Fonts, Stylesheets

Use csp.resourceDomains to allow loading resources from specific origins. The Bonsai Shop example demonstrates this:

πŸ“„Bonsai Shop: External Imagestypescript
1// Get current origin for images
2const origin = typeof window !== 'undefined' ? window.location.origin : '';
3
4_meta: {
5 ui: {
6 resourceUri: 'ui://bonsai/product-card',
7 csp: {
8 resourceDomains: origin ? [origin] : [] // Allow images from same origin
9 }
10 }
11}

For External API Calls (fetch)

Use csp.connectDomains to allow fetch/XHR requests to specific APIs:

πŸ“„Example: API Accesstypescript
1_meta: {
2 ui: {
3 resourceUri: 'ui://my-app/widget',
4 csp: {
5 connectDomains: ['https://api.myapp.com', 'https://cdn.example.com']
6 }
7 }
8}
9
10// In your widget:
11const response = await fetch('https://api.myapp.com/data');