Skip to main content

Vue MCP Apps

Add a credential-free Vue interface to an MCP tool without changing its authorization boundary.

@lupinum/better-convex-vue/mcp-app is an experimental Vue lifecycle integration for the official MCP Apps SDK. It lets a supporting host render a ui:// resource in a sandboxed iframe while the same tool remains useful to clients that do not support Apps.

The integration is intentionally narrow. The official SDK owns App Bridge and wire behavior. Better Convex owns Vue mount, disposal, readonly reactive projections, and structured-clone boundaries. Your application still owns the tool, resource, authorization, data, and effects.

Install the optional entry

Install the complete exact Apps peer set only when the application builds an MCP App:

bash
pnpm add @lupinum/better-convex-vue@0.8.0-beta.40 convex@1.42.2 vue@3.5.40 @modelcontextprotocol/ext-apps@1.7.5 @modelcontextprotocol/sdk@1.30.0 zod@4.4.3

Importing ordinary @lupinum/better-convex-vue does not pull the Apps SDK into the client graph. The SDK and Zod entries satisfy the non-optional peers declared by the official Apps package under strict package managers.

Own one App per Vue scope

Create the lifecycle synchronously in component setup:

vue
<script setup lang="ts">
import { useMcpApp } from '@lupinum/better-convex-vue/mcp-app'
import type { McpAppError } from '@lupinum/better-convex-vue/mcp-app'
import { computed, watch } from 'vue'

const app = useMcpApp({
  implementation: {
    name: 'notes-dashboard',
    version: '0.1.0',
  },
  capabilities: {},
})

const canSearch = computed(() => app.phase.value === 'ready' && app.toolInput.value !== undefined)

watch(app.toolResult, (result) => {
  // Validate and project only the fields this interface renders.
  receiveSearchResult(result)
})

watch(app.error, (error: McpAppError | undefined) => {
  if (error) showSafeLifecycleFailure(error.code)
})

async function refresh() {
  if (!canSearch.value) return
  const result = await app.callServerTool({
    name: 'search_notes',
    arguments: validatedSearchInput(),
  })
  receiveSearchResult(result)
}
</script>

useMcpApp() must run synchronously during browser component setup. It fails before constructing the official App when called during SSR or outside an active component instance. It exposes one canonical phase, one readonly sanitized lifecycle error, readonly shallow refs for official host and tool notifications, and only two host operations:

  • callServerTool() asks the host to perform an ordinary MCP tool call;
  • openLink() asks the host to navigate to an external URL.

Both operations reject before initialization, structured-clone inputs and outputs, and reject results that complete after the Vue scope is retired. A tool-call or link rejection belongs to that operation: it rejects the caller without changing the connection phase or lifecycle error. Scope disposal removes Better Convex listeners, clears projected state, and closes the private SDK App exactly once. The mutable SDK App is never exposed.

Connection and structured-clone failures are terminal. They close the private App and expose only stable local diagnostics:

CodeMeaning
MCP_APP_CONNECT_FAILEDThe App could not complete the host handshake
MCP_APP_CLONE_FAILEDA bridge value could not be safely copied

The error object never includes the host error, rejected value, raw cause, stack, bridge transcript, or credentials. Log application-owned context around the stable code if you need telemetry; do not reconstruct a raw-host escape hatch. Host-context change notifications remain partial in the protocol. The official App merges them into its current context before this composable clones the merged snapshot.

Automatic resizing is deliberately unavailable. The exact SDK does not expose the cleanup required to retire its resize observer, so Better Convex constructs the App with automatic resizing disabled.

Register one truthful resource

Register the ui:// resource and its tool metadata directly with the official MCP server created by @lupinum/better-convex-mcp. Better Convex does not add another Apps registry or server protocol wrapper.

The HTML document should be a bounded production bundle with:

  • a restrictive Content Security Policy;
  • unused validation-library locale barrels excluded from the browser bundle;
  • no dynamic application data interpolated into executable HTML;
  • no credentials, provider references, raw errors, or Convex client;
  • validated rendering of every tool input and result; and
  • useful text and structured tool output for clients that do not render it.

Capability negotiation controls whether the host presents the App. It must not fork the canonical tool/resource inventory or change application authority.

Authorization remains ordinary MCP authorization

An iframe button grants no permission. callServerTool() goes through the host, the MCP bearer boundary, the tool schema, and current application authorization just like a model-originated call.

The iframe must never receive:

  • an MCP bearer token;
  • a Better Auth cookie, session, or provider-private identifier;
  • a Convex JWT or raw Convex client;
  • a refresh token or internal service proof;
  • application signing keys;
  • raw request headers, arguments from another operation, or raw error causes.

Recheck current membership, role, delegation, credential state, and target resource inside the canonical application operation. OAuth scopes and App visibility are ceilings and presentation metadata—not current application authorization.

openLink() is host-mediated navigation. A successful link response does not mean the human approved anything and does not grant the opened page authority.

Ordinary writes should remain ordinary tools. If an application requires step-up authentication or a human review for a high-blast-radius effect, the authoritative application must authenticate the person, reload current authority and impact, and perform its own state-changing request. The App can display validated impact and navigate to that application; it cannot approve or execute the protected effect merely because it rendered a button.

Progressive fallback

Always return useful model-visible content for a model-visible tool:

ts
return {
  content: [
    {
      type: 'text',
      text: 'Found 3 notes. The dashboard can refine these results.',
    },
  ],
  structuredContent: {
    matches: minimizedMatches,
  },
}

A baseline client can use this result without the extension. A capable host may also render the App resource and deliver the same validated result through App Bridge. Host denial, missing capability, iframe teardown, or link denial must not corrupt the canonical operation or fabricate success.

Experimental limits

The mcp-app entry remains experimental:

  • @modelcontextprotocol/ext-apps@1.7.5 logs parsed and sent bridge messages and exposes no logger control;
  • different-origin and compatible real-host evidence is still required; and
  • the Apps server helper is coupled to the v1 combined MCP SDK while @lupinum/better-convex-mcp uses the locked split v2 SDK for the final protocol.

Better Convex therefore uses the official Apps client directly and registers server resources through the selected official v2 server. It does not ship two MCP server runtimes, cast between SDK majors, or provide a compatibility protocol.

The exact beta candidate has passed production Vite browser proofs for a neutral notes dashboard and Ginko's materially different publish-impact interface, including installed-byte checks, fallback behavior, CSP/sandbox, wrong-source messages, malicious results, teardown, current authorization, and credential sentinels. Those local proofs do not substitute for protected deployment and real-host evidence before stable admission.