Migrating to mcp-use 2.x
This release moves the toolkit from mcp-use 1.34 to the 2.x line and onto mcp-use's native view system: views are first-class (ToolDefinition.view / .visibility), mcp-use owns the ui://views/<name>.html resources and the _meta.ui wire keys, and the mcp-use CLI (mcp-use dev / build / start) becomes the standard way to develop and serve a toolkit server. The guide below walks every breaking change with before/after snippets.
Who this is for: consumers of @miragon/mcp-toolkit-core / @miragon/mcp-toolkit-ui 0.10.x (built against mcp-use 1.34). The mcp-use 2.x line first shipped as 0.11.0 and is promoted unchanged to 1.0.0 — if you are already on 0.11.0, there is nothing further to migrate.
1. Bump the dependencies
| Package | Before | After |
|---|---|---|
mcp-use | 1.34.1 | 2.7.3 |
@modelcontextprotocol/sdk | 1.29.0 | removed — no longer a peer |
zod | 4.4.3 | ^4.4.0 (a peer range) |
| Node.js | — | >=22.22.2 (engines) |
mcp-use stays pinned exactly (a second copy in the graph breaks the widgets' host context); zod, and react, react-dom and tailwindcss for the UI package, are peer ranges, so your own versions dedupe against them.
Pin lucide-react to one version everywhere
mcp-use 2.x's graph carries a lucide-react@^0.562.0 peer (via the built-in inspector). If your app resolves a different lucide-react than @miragon/mcp-toolkit-ui does, pnpm splits mcp-use into two peer instances, and the bundled mcp-use/react view runtime exists twice. The symptom is a widget that crashes at render time with mcp-use/react hooks require a browser view mounted by bootstrapView even though the view is mounted (the 2.x runtime context is module-scoped; the 1.x window.openai global masked this failure mode). Pin lucide-react to the exact version @miragon/mcp-toolkit-ui depends on (see its package.json; 1.47.0 in 2.7), and add resolve.dedupe: ["mcp-use", "react", "react-dom"] to any Vite config that bundles widgets as a second guard. In a pnpm workspace with several packages, also pin the inspector's peer itself so every package resolves the same variant:
# pnpm-workspace.yaml
overrides:
"@mcp-use/inspector>lucide-react": 1.47.02. Update the server import path
The server runtime moved from the mcp-use/server subpath to the root entry:
// before
import { MCPServer, text, object, error } from "mcp-use/server"
// after
import { MCPServer } from "mcp-use"
import { textResult, objectResult, errorResult } from "@miragon/mcp-toolkit-core/tools"The v1 response helpers (text / object / error) are deprecated upstream; the toolkit ships wire-identical replacements (textResult / objectResult / errorResult) from @miragon/mcp-toolkit-core/tools.
3. Choose your serving path
2.x has two supported shapes. Pick one before touching the rest.
3a. The standard path — plain mcp-use project, toolkit on top (new)
You own a normal mcp-use project and the CLI owns view building and serving. This is the recommended target for most servers:
// index.ts
import { MCPServer } from "mcp-use"
import { installToolkit } from "@miragon/mcp-toolkit-core/tools"
import { createTasksPlugin } from "./modules/tasks/plugin.js"
const server: MCPServer = new MCPServer({ name: "my-mcp", version: "1.0.0" })
installToolkit(server, { modules: [createTasksPlugin()] })
server.tool({ name: "echo" /* … */ }, handler) // your plain tools — after the install
export default serverpackage.jsonneeds"main": "index.ts"— the CLI readsnameandmainfrom the project directory.- One
views/<name>/view.tsxper view-bound tool (views/render-view/plus one per model-visible widget tool), each default-exporting<McpToolkitApp widgets={widgets} />. The CLI mounts the default export viabootstrapView— you do not callmountMcpToolkitApphere. - Shared browser modules (the widget map, the Tailwind entry) must live under
views/(e.g.views/shared/widgets.tsx): the CLI dev server routes onlyviews/*through its Vite middleware, so a root-level shared module 404s in dev. Server-side files (index.ts, modules) are unaffected. - Develop with
mcp-use dev(HMR + built-in inspector at…/mcp/inspector), ship withmcp-use build/mcp-use start.
The runnable reference is examples/standalone-host.
3b. The Node adapter — createFrameworkApp (changed)
Keep this when the server must run in your own process or ship its views inline in the MCP resources (gateways that only forward the JSON-RPC endpoint). The options changed:
// before
const app = await createFrameworkApp({
name: "my-host",
baseUrl: process.env.MCP_URL,
plugins,
app: {
resourceUri: "ui://my-host/mcp-app.html",
htmlPath: path.join(here, "dist", "index.html"),
},
})
// after
const app = await createFrameworkApp({
name: "my-host",
// no baseUrl: mcp-use resolves the serving origin from the request (or MCP_URL)
plugins,
app: {
bundle: {
jsPath: path.join(here, "dist", "mcp-app.js"),
cssPath: path.join(here, "dist", "mcp-app.css"),
},
},
})app.htmlPath/app.resourceUri/baseUrlare gone;app.bundlepoints at a compiled ES module + stylesheet.- The return type is
MCPServer(wasMcpServerInstance<…>). - The bundle is read once at boot (1.x re-read the HTML per request): after rebuilding the bundle, restart the host.
- The widget-bundle Vite config changes from a single-file HTML to one JS + one CSS output — drop
vite-plugin-singlefile, add:
// vite.config.ts of the widget bundle
export default defineConfig({
plugins: [react(), tailwindcss()],
resolve: { dedupe: ["mcp-use", "react", "react-dom"] },
build: {
assetsInlineLimit: 100_000_000,
cssCodeSplit: false,
rollupOptions: {
input: path.join(here, "main.tsx"),
output: {
entryFileNames: "mcp-app.js",
assetFileNames: "mcp-app.[ext]",
inlineDynamicImports: true,
},
},
},
})- The bundle entry point mounts via the view runtime now:
// before (main.tsx)
createRoot(document.getElementById("root")!).render(<McpToolkitApp widgets={widgets} />)
// after
import { mountMcpToolkitApp } from "@miragon/mcp-toolkit-ui/app"
mountMcpToolkitApp({ widgets })4. Tool registration
Input schemas
schema: → inputSchema: on every server.tool definition (mcp-use 2.x field name).
Widget-rendering tools (model-visible)
uiMeta({ resourceUri }) is gone. Bind a native view (named after the tool) and stamp only the Apps SDK half:
// before
server.tool(
{
name: "show_tasks_board",
schema: z.object({}),
_meta: uiMeta({ resourceUri }),
},
handler,
)
// after
import { appsSdkMeta, viewResourceUri } from "@miragon/mcp-toolkit-core"
server.tool(
{
name: "show_tasks_board",
inputSchema: z.object({}),
view: { name: "show_tasks_board" }, // mcp-use emits _meta.ui.* from this
outputSchema: z.object({}).passthrough(), // a view binding requires one
_meta: appsSdkMeta({
// the openai/* half for Apps SDK hosts
resourceUri: viewResourceUri("show_tasks_board"),
title: "Task board",
}),
},
handler,
)Never stamp _meta.ui.* by hand — mcp-use owns that namespace and overwrites it on tools/list.
App-only tools (*_data feeds, refresh hooks)
// before
server.tool({ name: "tasks_board_data", schema: z.object({}), _meta: APP_ONLY_META }, handler)
// after
server.tool({ name: "tasks_board_data", inputSchema: z.object({}), visibility: "app" }, handler)Registrar API changes
AppPlugin.registerWidgetTools(server, resourceUri, metaDefaults?)→registerWidgetTools(server, metaDefaults?)— the resource URI parameter is gone (views are named after tools).createWidgetToolRegistrar(server, client, resourceUri, metaDefaults?)→createWidgetToolRegistrar(server, client, metaDefaults?). Model-visible tools get the view binding +openai/*keys automatically; app-only tools getvisibility: "app".WidgetToolVisibilitynarrowed to"app" | "model"(the array form is gone).- Removed exports:
uiMeta,APP_ONLY_META,buildAppResourceCsp,deriveAppResourceUri,installToolCallNameCapture(plus its types). Replacements:appsSdkMeta,viewResourceUri,VIEW_RESOURCE_URI_PREFIX.
CSP
app.csp still exists but only needs third-party origins (a CDN, an API the widget fetches directly): mcp-use appends the request-resolved server origin to the view-resource CSP itself. The baseUrl-origin injection (buildAppResourceCsp) is gone with baseUrl.
5. UI package changes
Host bridge
useHostBridge()now throws without a provider (2.x has no provider-less fallback). Inside the toolkit shell nothing changes —McpToolkitAppinstalls the provider. Bare unit renders must wrap inHostBridgeProvider+createStandaloneHostBridge.useMcpUseHostBridge/createMcpUseHostBridgeare removed. The mcp-use adapter is nowMcpUseHostBridgeProvider(a component, used byMcpToolkitApp) over the pure mappingtoHostBridge(surface).
Model context
Widgets must never import ModelContext from mcp-use/react — in 2.x it throws in every mount that is not a bootstrapView view (the widget playground, ChatGPT, standalone apps). Use the host-portable replacement:
// before
import { ModelContext } from "mcp-use/react"
return <ModelContext content={describe(data)}>{body}</ModelContext>
// after
import { HostModelContext } from "@miragon/mcp-toolkit-ui/app"
return <HostModelContext content={describe(data)}>{body}</HostModelContext>Inside an mcp-use view it renders the native aggregating ModelContext; everywhere else it reports through the HostBridge's setModelContext. (adaptDataWidget's describeForModel already routes through it.)
Fixture harness
WidgetFixtureHost no longer installs a window.openai shim — it simulates the portable HostBridge surface only. A widget that calls mcp-use/react hooks directly must be exercised against a real host (mcp-use dev) — or, better, rewritten against useHostBridge.
6. Middleware and infrastructure
- Role filter:
resolveToolName/resolveToolNamesoptions are gone — 2.x delivers the tool name onctx.params.nameand firesmcp:tools/callmiddleware once per call (batch entries are guarded individually).failClosedkeeps its meaning. - Caller identity: 2.x hands middleware the SDK
AuthInfo(mapped user underctx.auth.extra.user) and callbacks the flattenedctx.auth.user; the built-in providers mapidandorganizationId, notuserId/organization_id. Read the caller withresolveCaller/resolveCallerIdfrom@miragon/mcp-toolkit-core(toolkit ≥ 2.6 does so in the role filter, org gate, dashboards and pipeline ctx) — a provider wrapper that copiesidtouserIdis no longer needed. See Middleware and auth. - Dashboards saved under OAuth by toolkit ≤ 2.5 carry no owner (2.5 read no
id), so toolkit ≥ 2.6 serves them to no identified caller. Re-own or remove them as described in Upgrading from toolkit 2.5. - Backend registry: the per-session sticky selection is gone entirely — 2.x serves HTTP statelessly (no session ids to key on), and in-memory selection state breaks behind more than one replica.
resolve(id?)takes an explicit id or falls back to the single configured backend; a server that wants a sticky/default backend resolves that id from its own durable store (e.g. a per-user profile) and passes it in. Single-backend setups are unaffected. - Inspector: the programmatic host no longer serves
/inspector. The inspector ships withmcp-use dev(standard path), or use the hosted one — which needs CORS enabled on your server (serverOptions: { cors: … }).
7. Migration checklist
- [ ] Bump
mcp-useto2.7.3, drop the@modelcontextprotocol/sdkpeer; Node>=22.22.2. - [ ] Pin
lucide-reactto the version@miragon/mcp-toolkit-uidepends on (one version across the graph). - [ ]
mcp-use/serverimports →mcp-use;text/object/error→textResult/objectResult/errorResult. - [ ] Pick a path:
installToolkit+views/+ CLI (standard) orcreateFrameworkApp+app.bundle(adapter). - [ ]
schema:→inputSchema:everywhere. - [ ] Widget tools:
view:+outputSchema+appsSdkMeta/viewResourceUri; app-only tools:visibility: "app". - [ ] Drop the
resourceUriparameter fromregisterWidgetTools/createWidgetToolRegistrar. - [ ] Bundle entry →
mountMcpToolkitApp(adapter path) /McpToolkitAppdefault export per view (CLI path). - [ ] Replace direct
ModelContextimports in widgets withHostModelContext. - [ ] Wrap bare widget renders in a
HostBridgeProvider(no provider-less fallback). - [ ] Multi-backend servers: resolve the caller's default backend yourself (durable store) and pass the id to
resolve— the registry keeps no session state. - [ ] Adapter path: restart the host after every widget-bundle rebuild.