Skip to main content
Upgrade an existing mcp-use 1.x server by updating the runtime, server entry, registrations, request context, security configuration, and widgets. Most server registrations keep the same concepts and require small syntax changes. Widgets require a complete rewrite as MCP App Views.

Migration checklist

1

Update the runtime and server entry

Upgrade to Node.js >=22.22.2, use ESM, import from mcp-use, and default-export the server.
2

Update server registrations

Convert tools, resources, resource templates, and prompts to the definition-plus-callback API and return raw MCP results.
3

Update runtime behavior

Replace session assumptions, request context fields, authentication, notifications, elicitation, middleware, and deployment adapters.
4

Rebuild widgets as Views

Replace the widget file layout, tool binding, result helper, provider, generated helpers, hooks, state model, and asset paths.
5

Review v2 limitations and verify

Check whether a missing v1 capability affects the server, then typecheck, build, run, and exercise the migrated project.

Update the runtime and entry point

mcp-use v2 requires Node.js >=22.22.2 and ESM.
Import server APIs from mcp-use:
Default-export the server from index.ts. v1 getHandler() remains only as a deprecated alias for server.fetch. Pass server.fetch to edge and Fetch-based runtimes directly. To serve from a custom Node.js http server, wrap the server with toNodeHandler():

Export every static tool

Assign every statically declared tool to an exported constant. If tools live in other modules, re-export them from the server entry:
Apply this rule to all static tools, not only tools currently bound to or called by a View. The root mcp-env.d.ts declaration registers typeof import("<server-entry>"). Only exported ToolRef values participate in View typing. This is a live type-only import, not the v1 tool-registry generator. mcp-use dev, build, and typecheck create or refresh the declaration, but they do not execute the server to generate tool-specific types. If the v1 server registers a tool from a loop, runtime configuration, or an OpenAPI document, it cannot export a static ToolRef. Call that tool from a View with explicit types:
Do not import a server-side ToolRef value into a file-based View. The View bundle must not include server code.

Migrate tools

Move the callback to the second argument, replace schema with inputSchema, and return a raw MCP result.
schema remains an alias for inputSchema, but inputSchema matches the MCP wire field and is the migration target. Follow these result rules:
  • A successful tool with outputSchema must return matching structuredContent.
  • An error result may return isError: true without structured output.
  • For object-shaped structured output, include a text fallback in content.
  • Replace inputs[] with a Standard Schema-compatible input schema.
  • Do not chain server.tool(...).tool(...); tool() returns a ToolRef.
Response helpers such as text(), object(), array(), image(), and error() remain as deprecated upgrade shims. Prefer raw CallToolResult, ReadResourceResult, and GetPromptResult envelopes. Native array() does not preserve the v1 { data } wrapper.

Migrate resources and templates

Pass the reader as the second argument. A static resource callback receives (uri, ctx) and returns contents:
Remove inline readCallback and callbacks fields. For resource templates:
  • replace nested resourceTemplate with top-level uriTemplate
  • move callbacks.complete to top-level complete
  • pass the reader separately as (uri, params, ctx)
  • handle inferred template values as string | string[]
  • return raw contents

Migrate prompts

Keep schema for prompt arguments, pass the callback separately, and return raw messages:
Replace v1 args[] and inline cb. completable() supplies suggestions but does not constrain valid values. Apply field refinements before wrapping a schema with completable().

Update request context

v2 request context exists only for the current request. Remove session assumptions from client metadata, logging, progress, notifications, and elicitation. ctx.client.info(), capabilities(), can(), extension(), and user() return client-declared request hints. Never use them for authorization. Use verified ctx.auth data for identity and permissions.

Send notifications on the correct channel

v2 supports custom notifications during an active request:
Await the notification before the callback returns. It travels on the originating request’s response stream and cannot notify that client after the request finishes. Use the standard request-scoped and subscription APIs for their specific jobs:
The server helpers notify matching active subscription listeners. They do not provide arbitrary broadcast or session-targeted push.

Make elicitation replay-safe

v2 elicitation returns an input-required result and re-runs the handler when the client supplies input:
Use a stable key. Assume the handler can run more than once. Do not perform a side effect before accepted input is available.

Rebuild widgets as MCP App Views

Legacy widgets require a full UI migration. File discovery, binding, tool results, component lifecycle, hooks, state, assets, and typing all changed.

Move the files and binding

Move data previously passed as props to the tool result’s _meta field. Access it from useToolContext().meta in the View. _meta is View-only and is not validated or typed by outputSchema; validate or narrow it before use. Keep structuredContent for output that matches outputSchema and should also be visible to the model. The bound tool must:
  • export its ToolRef
  • declare an outputSchema
  • return matching structuredContent
  • move v1 widget({ props }) data to result _meta
  • return useful text content for the model and text-only hosts
  • bind to exactly one View with view: { name }
Each View can bind to at most one server tool. Create another View when a second tool needs a rendered result. Keep helper tools viewless and call them from the View with useCallTool(). Move resource facts such as description, csp, permissions, domain, and prefersBorder into the bound tool’s view configuration. Remove v1 invoking, invoked, and widgetAccessible; the native View configuration does not emit those OpenAI-specific overlay fields.

Replace widget hooks

Default-export the View component. It mounts once while the host connects and while tool input and result notifications arrive.
The error status also covers a host that cancels the tool call before a result arrives. view.error is then a ToolCancelledError; check view.error instanceof ToolError before reading view.error.result. Import both classes from mcp-use/react. Use the focused v2 surface: viewConfig supports autoResize, displayModes, and preferredDisplayMode. The values below are the defaults for autoResize and displayModes, so omit the export when they are correct for the View:
displayModes must include "inline". preferredDisplayMode accepts "inline" or "fullscreen", has no default, and must appear in displayModes; ChatGPT may ignore it. See resource display modes. Use useSendSizeChanged() only when autoResize is false and the View reports its size manually. Compose optional presentation components such as ThemeProvider, ViewControls, and your own error boundary. The framework owns the connection, bootstrap, and required top-level error boundary.

Migrate View state and model context

useViewState() requires an object default and shares one JSON-serializable snapshot across the mounted View. The state is model-visible. ChatGPT can restore View state across View lifetimes. Other MCP Apps hosts keep the state only for the current iframe lifetime. Use the declarative <ModelContext> component for model-visible natural-language context. v2 removes the imperative modelContext.set(), modelContext.remove(), and modelContext.clear() helpers; render <ModelContext> where the annotation applies so it follows React state and component lifecycle. Use useSendFollowUp() to trigger a new model turn. Keep presentation-only state in ordinary React state. useFiles() keeps the upload/download capability check, but it no longer accepts the v1 modelVisible option or mutates widget state automatically.

Update View assets

Native View documents are generated MCP resources. Do not register uiResource() or serve a separate HTML document. Move existing widget CSP domains to view.csp. Set view.domain to the HTTPS UI origin for ChatGPT. Claude resource reads derive the sandbox domain from the full MCP endpoint, using MCP_URL plus basePath or the public request URL. Remove response helpers that overwrite generated _meta.ui.domain. See UI origin and endpoint migration for legacy URL values, proxy configuration, and metadata precedence.

Handle View URIs cached by ChatGPT

v1 served widgets at ui://widget/<name>.html, and sometimes ui://widget/<name>.<buildId>.html; v2 serves Views at exactly ui://views/<name>.html. A published ChatGPT app caches the resource URI from the v1 tool metadata and keeps requesting the old URI after you deploy v2, so its Views fail to load until the app is resubmitted and re-approved with the new metadata. As a temporary bridge, add middleware that rewrites the legacy URI before the request reaches the MCP handler and restores the requested URI in the response:
This shim is temporary. Submit a new version of the published app so ChatGPT picks up the v2 tool metadata and starts requesting ui://views/<name>.html directly. Once the new submission is approved and live, remove the middleware.

Update authentication, proxies, and security

Native v2 acts as an OAuth resource server backed by an external authorization server. Import provider adapters from mcp-use/oauth/*, supply their required locator options, and use the provider-specific ctx.auth.user type. For a provider without Dynamic Client Registration, such as GitHub or Google, oauthProxy makes the server its own authorization server. See Migrate oauthProxy. Replace generic userId assumptions with the provider’s mapped field, commonly id. Replace getAuth, hasScope, and requireScope with verified ctx.auth, scopes, permissions, and application authorization checks. To serve public, optional, and sign-in tools from one OAuth-enabled endpoint, set mixedAuth: true on the server and declare securitySchemes on each tool. Without mixedAuth, every request to the MCP endpoint needs a valid token. If a v1 tool sets _meta.securitySchemes by hand, v2 passes it through unchanged but does not enforce it; declare securitySchemes on the tool instead. See Mixed authentication. jwksVerifier and mountOAuthProxy have no v2 equivalent. v2 upstream composition accepts HTTP remotes or a ready compatible client connection. A v1 stdio child-process proxy requires an architecture change.

Preserve security semantics explicitly

The name allowedOrigins changed meaning: Map v1 CORS allowMethods and allowHeaders to v2 methods and allowedHeaders. Test Host rejection, Origin rejection, and CORS separately. Use basePath for the MCP route. Set MCP_URL for the externally visible server origin.

Keep existing OAuth connections working

v2 binds every access token to the MCP endpoint instead of the server origin. Clients that authorized against a v1 server can hold tokens that v2 rejects. Plan this migration before you deploy v2 to an existing production URL. A token whose aud is only the v1 identifier fails with Token audience does not include the protected resource, and the client receives HTTP 401. The v2 identifier always ends with basePath, so you cannot configure v2 to advertise the bare origin. Which clients are affected depends on where the client gets its resource identifier:
  • Clients that read protected-resource metadata follow the 401 challenge and request tokens for the v2 identifier when they next authorize.
  • Clients with a saved resource setting keep requesting the v1 identifier. A published ChatGPT or Codex app uses the Resource value from its approved app submission. Reconnecting does not help, because every new token has the v1 audience. OpenAI tool scans that use the same saved value fail with the same error.
  • Clients that refresh tokens get whichever audience your authorization server issues for the resource they send on refresh. A client that keeps sending the v1 identifier can keep getting v1-audience tokens indefinitely when refresh tokens rotate.
1

Find the identifier your v1 clients use

Check the resource value in your v1 protected-resource metadata, or the aud claim of a current access token. It is usually your MCP_URL origin.
2

Accept both identifiers during the migration

Pass both identifiers to createJwtVerifier through audience. The server still advertises only the v2 identifier, and still verifies the signature, issuer, and expiry.
3

Update clients that save a resource setting

Submit a new version of each published app with the v2 identifier as its Resource. Keep accepting the v1 identifier until that version is approved and live. Submitting it is not enough.
4

Allow the v2 identifier on the authorization server

Confirm that the authorization server issues tokens with the v2 identifier as their audience. For the Better Auth OAuth provider plugin, include it in validAudiences.
5

Remove the v1 identifier after the last v1 token expires

Log when an accepted token’s aud does not include the v2 identifier. The verified claims are available in authInfo.extra.payload. Remove the v1 identifier only after no v1-audience tokens appear for at least your longest access-token lifetime.
This example accepts both identifiers with the Better Auth provider:
server.ts
The audience option checks the aud claim only. If your authorization server also sets a resource claim to the v1 identifier, v2 still rejects the token. Verify the migration with a real token that a v1 client holds.
Removing the v1 identifier before every client has moved breaks those connections immediately. Published apps show a reconnect prompt that cannot succeed, because each reconnect issues another v1-audience token.
The resource identifier is part of your public contract. Changing MCP_URL or basePath later changes it again and requires the same migration.

Migrate oauthProxy

v2 keeps oauthProxy for providers without Dynamic Client Registration, such as GitHub and Google, with a new API and a new token model:
  • Import it from mcp-use/oauth. Configure the provider with github(), google(), or upstream() instead of authEndpoint and tokenEndpoint.
  • The proxy issues its own tokens. Tools read the upstream token with upstreamToken(ctx.auth); verifyToken and jwksVerifier are removed.
  • Outside localhost, the proxy requires a secret (or MCP_USE_OAUTH_SECRET) and a shared storage.
  • The upstream callback URL moves from /oauth/callback to /mcp/oauth/callback.
  • Every user approves the consent page and signs in once more, in the same client: v2 rejects the upstream tokens that v1 clients hold. Redirect the v1 /authorize and /token paths for clients that saved them.
Follow Migrate from v1 oauthProxy for the option mapping, a before-and-after example, and the deployment steps.

Review v2 limitations

The following v1 capabilities do not currently have direct v2 equivalents. Update the application design where one of these limitations affects your server.

Server lifecycle and transport

  • Session stores, active-session registries, stream managers, session recovery, and session affinity are not part of v2.
  • Arbitrary session-targeted and post-response push are not available. Request-scoped custom notifications and subscription invalidations remain available.
  • Blocking server-to-client sampling through ctx.sample() is not available. Return an inputRequired.createMessage() round instead; see Sampling.
  • The v1 roots-changed callback surface is not available.
  • Express and Connect adapters are not available. Use Hono, server.fetch, or toNodeHandler() from mcp-use/node.
  • Stdio serving and stdio child-process proxy configuration do not have a native migration path.
  • CommonJS and unsupported Node.js versions are intentional compatibility breaks.
Move durable state to application storage keyed by verified user, tenant, or an explicit domain handle. Do not key business state to an MCP transport session.

OAuth, proxy, and OpenAPI

  • oauthProxy is available for providers without Dynamic Client Registration, with a new API and its own tokens. A stand-alone authorization server with its own user accounts is not provided.
  • mountOAuthProxy, the browser OAuth helper routes, has no equivalent.
  • Unsigned token bypasses such as verifyJwt: false are not supported.
  • Verify proxy requirements for upstream OAuth, templates, completions, subscriptions, list synchronization, sampling, and elicitation before migrating.
  • Verify OpenAPI servers that depend on external references, cookies, non-JSON request bodies, callbacks, webhooks, or automatic security-scheme discovery.

Views

  • Each View can bind to at most one server tool.
  • A bound tool requires outputSchema and matching structuredContent.
  • Static typing sees exported ToolRef values only.
  • Cross-lifetime useViewState() restoration is host-specific.
  • useFiles() is host-specific and must be capability-checked.
  • Raw uiResource, @mcp-ui/server adapters, exposeAsTool, and a shared View bound to multiple server tools have no native equivalent.

Verify the migration

1

Run a real TypeScript check

Run mcp-use typecheck, npm run typecheck, or the project’s actual tsc --noEmit command. mcp-use build bundles code but does not prove that v1 APIs are type-compatible.
2

Build and start the production output

Run the project’s production build, start the built server, and confirm the expected MCP route and public asset routes.
3

Exercise server primitives

Call at least one tool, read a static resource, complete and read a resource template, and get a prompt with a real client.
4

Render every View state

Verify pending, ready, and error states; matching structured output; theme; display mode; sizing; CSP; and public assets in a capable host.
5

Test security and authentication

Check a valid token, a token issued before the migration, missing or invalid scopes, mapped user data, Host rejection, Origin rejection, and browser CORS behavior.
6

Preserve behavioral coverage

Keep existing runtime assertions as runtime assertions. If v2 does not support a tested behavior, leave that limitation explicit instead of replacing the test with source-text or artifact assertions.
7

Remove v1 leftovers

Remove remaining v1 imports, helpers, widget files, session APIs, ctx.sample() calls, stdio proxy configuration, and old security fields. No mcp-use/server import can stay: that subpath does not exist in v2.

Continue with the v2 guides