Changelog

What's new in Skybridge.

Every Skybridge release, sourced directly from GitHub. New features, fixes, and breaking changes.

  1. v2.0View on GitHub →

    North Face

    No lift runs to the north face. You climb it, and you pick the line yourself because nobody has left tracks in it.

    MCP shipped the 2026-07-28 revision this summer and no framework was on it yet. Getting there meant rebuilding the part of Skybridge nobody sees: how a request becomes a server. One /mcp endpoint now serves both the 2025-era hosts you already support and the new revision, and every request gets its own server instance instead of sharing one long-lived singleton.

    The other unmarked line is testing. A tool that works is not the same as a tool the model decides to call, and until now there was no way to check the difference. @skybridge/test runs a real conversation against your app and asserts on the calls the model actually made.

    Alongside those: schema validation that is no longer zod-only, and a hooks surface that groups things where you expect to find them.

    This is a major, so there are breaking changes.

    The full list, with before and after, is in the changelog below, and your coding agent can do most of it for you.


    MCP 2026-07-28, and why the server got rebuilt

    The new protocol revision changes how results are serialized, and in SDK v2 the negotiated era is state on the server instance. Skybridge used to keep one McpServer for the process lifetime, which meant that instance never negotiated anything: it would have served 2026-era clients with the 2025 codec, and one client's initialize would have overwritten another's.

    So registration moved into a handler the framework calls for each request. Your tools, resources, prompts and views are registered on the instance that serves the request, which is the one the SDK actually stamps with the negotiated version.

    You see this as the new Skybridge app. The implementation info, the SDK options and Skybridge's own options merge into one config object, and the tool chain moves into its handler field:

    // before
    const server = new McpServer(
      { name: "my-app", version: "1.0.0" },
      { capabilities: {} },
      { oauth },
    ).registerTool({ name: "search", ... }, handler);
    
    export default await server.run();
    export type AppType = typeof server;
    
    // src/server.ts, after
    export const app = new Skybridge({
      name: "my-app",
      version: "1.0.0",
      oauth: descopeProvider({ url: env.DESCOPE_URL }),
      handler: (server) => server.registerTool({ name: "search", ... }, searchHandler),
    });
    
    export type AppType = typeof app;
    
    // src/index.ts, after
    import { app } from "./server.js";
    
    export default await app.run();
    

    server.ts stays the complete definition of your app, index.ts only runs it, and tests can import the app without triggering the run.

    The handler must return the chained server, that is what carries your tool types into typeof app. Everything else is inferred from the config too: pass a provider to oauth and extra.http.authInfo.extra is typed with its claims in every tool handler, no annotation needed. Keep the handler inline for that to work; an extracted handler needs a hand-written McpServer type and loses the inference, which is why the SkybridgeServer alias from the v2 betas is gone.

    There is no module-scope server object any more, so anything you used to do to one after startup, registered.disable() or a raw setRequestHandler, moves inside the handler. Outside it there is nothing to call them on, which the compiler will tell you.

    mcpMiddleware follows the same rule: it is per-instance state now, so it chains inside the handler, and ordering within the chain is unchanged:

    export const app = new Skybridge({
      ...config,
      handler: (server) =>
        server
          .mcpMiddleware(intentMiddleware())
          .registerTool(...),
    });
    

    Keep the handler pure

    This is the one part of the change that can bite you in production, and neither the compiler nor a manual test will catch it. The handler body runs on every request, so registration is the only work that belongs inside it:

    // WRONG: one pool per request
    export const app = new Skybridge({
      ...config,
      handler: (server) => {
        const pool = new pg.Pool();
        return server.registerTool(...);
      },
    });
    
    // RIGHT: setup runs once, the handler receives its result
    export const app = new Skybridge({
      ...config,
      setup: () => new pg.Pool(),
      handler: (server, pool) => server.registerTool(...),
    });
    

    The same goes for file reads, config parsing and client construction. This matters most when migrating, because in v1 those statements sat at module scope in the same file and the tempting move is to wrap the lot in the handler. If a handler takes longer than 50ms, Skybridge warns once in the console.

    setup runs once, at run() or on the first request and never at module import, and its result is passed to the handler as the second argument. The handler stays synchronous:

    export const app = new Skybridge({
      ...config,
      setup: async () => loadConfig(),
      oauth: (cfg) => descopeProvider({ url: cfg.mcpServerUrl }),
      handler: (server, cfg) => server.registerTool({ name: cfg.toolName, ... }, searchHandler),
    });
    

    oauth takes a provider, a raw config object, or a function of the setup result returning either. Providers are deferred: descopeProvider(...) validates its arguments but performs no network call until run() resolves it, so importing server.ts from tests and evals never reaches your IdP.

    Evals

    A tool that works is not the same as a tool the model decides to call. @skybridge/test closes that gap: it runs a real conversation against your app, in process, and hands you the calls the model made.

    @skybridge/test ships as a beta alongside this release: install it with @skybridge/test@beta, expect the matcher API to move in minors, and pin the exact version in CI if that matters to you.

    import { anthropic } from "@ai-sdk/anthropic";
    import { start } from "@skybridge/test";
    import { expect, it } from "vitest";
    import { app } from "../src/server.js";
    
    it("reaches the capitals tool from a natural prompt", async () => {
      const chat = await start({ app, model: anthropic("claude-sonnet-4-5") });
      await chat.send("Tell me about the capital of France");
    
      expect.chat(chat).toHaveCalledToolWith("explore-capitals", { name: "Paris" });
    });
    

    toHaveCalledToolWith and toHaveCalledToolOnce are typed against your own registry, so the tool name autocompletes and the argument object is checked against that tool's input schema. toNeverHaveCalledTool, toHaveFailedToolCall and toHaveSaid cover the negative and conversational cases. Add evals: {} to the Vite plugin options to wire the matchers into vitest, then vitest run evals.

    No HTTP server, no port, no fixtures. The client dials your app through an in-process fetch, and each conversation gets its own MCP handler that closes with the test.

    Add @skybridge/test@beta (the package is published on the beta dist-tag only while the API settles, so a bare @skybridge/test will not install it), vitest, ai and the provider package you want (@ai-sdk/anthropic, @ai-sdk/openai, …) as dev dependencies, name your scenarios evals/*.eval.ts, and run them with vitest run evals. Turning on evals also raises the per-scenario timeout to two minutes, since each turn is a live model call, and loads your .env so the provider key is picked up. Tune the timeout with evals: { timeout }.

    For an app behind OAuth, claim an identity for the session:

    const chat = await start({
      app,
      model: anthropic("claude-sonnet-4-5"),
      authInfo: { token: "eval", clientId: "evals", scopes: ["read"] },
    });
    

    Only token verification is skipped. Per-tool schemes and scope checks run for real against those claims, and extra carries whatever your verifier would have produced. Omit authInfo to exercise the anonymous path, auth challenges included. The auth-descope example ships a scenario to start from.

    Standard Schema

    inputSchema and outputSchema accept any Standard Schema validator now, not just zod. Nothing changes if you are happy with zod. If you would rather use valibot or arktype, you can, and the inferred handler argument types follow.

    The schema has to advertise itself as JSON Schema too, since that is what tools/list sends to the host: the accepted type is the SDK's StandardSchemaWithJSON. Zod 4 and ArkType implement it out of the box; valibot does through @valibot/to-json-schema. A validator that cannot emit JSON Schema is a compile error, not a silent gap in your tool listing.

    One consequence of users bringing their own schemas: zod is a peer dependency of skybridge now, not a bundled copy, and it has to be 4.2 or later, since 4.0 and 4.1 make the SDK fall back to a converter that drops .describe() descriptions from the schema the model sees.

    Hooks, regrouped

    useLayout mixed two unrelated things: a user preference that rarely changes, and viewport geometry that changes on every resize. Views that only wanted theme re-rendered on every resize. They are split now: theme joins useUser, next to locale and userAgent, and maxHeight and safeArea move to the new useViewport.


    How to migrate?

    1. Install the latest version of the skill
    2. Run the prompt inside your favorite coding agent: Migrate my app based on https://github.com/alpic-ai/skybridge/releases/tag/v2.0.0

    Then validate, because a green build is not enough:

    1. Install dependencies and check for unmet peer warnings on @skybridge/devtools or @skybridge/vite-plugin.
    2. tsc --noEmit. This catches the handler return, the extra reshape, and every removed export.
    3. skybridge build, then skybridge start, not just skybridge dev: it is the only step that exercises the compiled dist/index.js entry.
    4. skybridge dev, open the view in devtools and confirm it renders.

    A missing src/index.ts passes tsc and skybridge build; skybridge dev and skybridge start both fail on it, unless a leftover nodemon.json still points dev at src/server.ts, in which case dev silently starts nothing. Delete that file, the CLI default already runs src/index.ts. An impure handler passes everything, devtools included, and only fails under sustained traffic.

    Having issues? Join our Discord to get help from the maintainers.


    Breaking changes

    Ordered by how likely a v1 app is to hit them. Each is enforced by the compiler unless noted.

    The Skybridge app replaces the McpServer singleton (PR #1008, #1070)

    // before
    const server = new McpServer(info, options, skybridgeOptions).registerTool(...);
    
    // after
    export const app = new Skybridge({ ...info, ...options, ...skybridgeOptions, handler: (server) => server.registerTool(...) });
    

    One config object. The tool chain moves into handler, which must return it; one-time work goes in setup, whose result is the handler's second argument; oauth takes a provider, a config, or a function of the setup result. mcpMiddleware chains inside the handler too.

    The server entry moves to src/index.ts (PR #1070)

    src/server.ts exports the app, a new src/index.ts runs it, and skybridge start looks for dist/index.js. Not caught by the compiler: a missing index.ts only fails at skybridge start.

    // before, src/server.ts
    export default await server.run();
    
    // after, src/index.ts
    import { app } from "./server.js";
    
    export default await app.run();
    

    skybridge/vite moves to @skybridge/vite-plugin (PR #1008)

    Install @skybridge/vite-plugin as a devDependency. The eval matchers live at @skybridge/vite-plugin/evals.

    // before
    import { skybridge } from "skybridge/vite";
    
    // after
    import { skybridge } from "@skybridge/vite-plugin";
    

    Dependencies: drop the SDK, add zod, bump devtools (PR #1008)

    Delete @modelcontextprotocol/sdk from your dependencies and import from skybridge/server, which re-exports ProtocolError, ProtocolErrorCode, OAuthError and OAuthErrorCode. Add zod at ^4.2.0, now a peer dependency (4.0 and 4.1 drop .describe() descriptions from the schema the model sees). Move @skybridge/devtools to v2 as well: skybridge v2 peer-depends on it, so a ^1.x pin reports an unmet peer.

    Tool handler extra is now the SDK's ServerContext (PR #1008)

    // before
    async (args, extra) => {
      const token = extra.authInfo?.token;
      const meta = extra._meta;
      extra.signal.throwIfAborted();
    }
    
    // after
    async (args, extra) => {
      const token = extra.http?.authInfo?.token;
      const meta = extra.mcpReq._meta;
      extra.mcpReq.signal.throwIfAborted();
    }
    

    signal, id, notify and send all move under extra.mcpReq. The typed ChatGPT client hints stay on extra.mcpReq._meta.

    The registerTool(name, config, handler) form is removed (PR #1008)

    Not caught by the compiler in v1 either: this form was accepted at runtime only, never typed. If your app has it, it already failed tsc; v2 also drops the runtime tolerance.

    // before
    server.registerTool("search", { inputSchema }, handler);
    
    // after
    server.registerTool({ name: "search", inputSchema }, handler);
    

    useLayout is removed (PR #1008)

    LayoutState becomes ViewportState.

    // before
    const { theme, maxHeight, safeArea } = useLayout();
    
    // after
    const { theme } = useUser();
    const { maxHeight, safeArea } = useViewport();
    

    useHostInfo is renamed useHost (PR #1008)

    Same return shape.

    useDownload returns the function bare (PR #1008)

    // before
    const { download } = useDownload();
    
    // after
    const download = useDownload();
    

    useToolInfo loses its idle state (PR #1008)

    status starts at "pending" and never was "idle" at runtime. ToolIdleState and the isIdle field are gone from every state in the union.

    // before
    const { isIdle, isPending } = useToolInfo("search");
    
    // after
    const { isPending } = useToolInfo("search");
    

    InvalidTokenError is removed (PR #1008)

    // before
    throw new InvalidTokenError("expired");
    
    // after
    throw new OAuthError("invalid_token", "expired");
    

    Providers return a deferred OAuthProvider (PR #1008)

    The branded providers and customProvider no longer return a promise. They return an OAuthProvider, an object whose resolve() runs discovery, and oauth accepts it directly. Drop the await; if you resolved a provider by hand to feed requireBearerAuth, call resolve().

    // before
    oauth: await workosProvider({ domain, audience }),
    const config = await customProvider({ issuer, audience });
    
    // after
    oauth: workosProvider({ domain, audience }),
    const config = await customProvider({ issuer, audience }).resolve();
    

    The oauth verify option is removed (PR #1008)

    verifier is the only path, and it types the claims your handlers receive.

    // before
    oauth: { verify: { jwksUri, issuer, audience } }
    
    // after
    oauth: { oauthMetadata, verifier: createJwksVerifier({ jwksUri, issuer, audience }) }
    

    KnownToolMeta.securitySchemes is removed (PR #1008)

    Declare security schemes through the top-level tool config or the auth field instead of _meta. Not caught by the compiler: ToolMeta stays open (Record<string, unknown>), so _meta: { securitySchemes } still typechecks, is forwarded to the client untouched, and no longer enforces anything.

    ViewConfig.hosts and window.skybridge.hostType are removed (PR #1008)

    Every view emits a single ext-apps resource, so host targeting no longer applies. ViewHostType is gone, and the served view page no longer declares hostType on window.skybridge; the runtime is detected at load time via window.openai. A view reading window.skybridge.hostType should drop the check.

    SDK v2 renames that reach tool handlers (PR #1008)

    McpError is ProtocolError and ErrorCode is ProtocolErrorCode. extra.http?.req is a Web Request, so header reads use .headers.get(). extra.mcpReq.send() takes no result schema for spec methods. extra.mcpReq.elicitInput() exists but is deprecated by the SDK and throws on a 2026-07-28 request; on the new revision, return inputRequired(...) (re-exported from skybridge/server) from the handler instead, as the SDK's upgrade guide describes. registerPrompt and registerResource follow the SDK signatures: the variadic .prompt()/.resource() forms are gone, argsSchema is a schema object, and registerResource takes a metadata argument. The other OAuth error classes collapse into OAuthError + OAuthErrorCode. Not caught by the compiler: unknown-tool calls now reject with ProtocolError instead of resolving isError: true, and error messages lose the MCP error <code>: prefix. The SDK's upgrade guide covers the rest.

    Registration and OAuth errors surface at first use (PR #1008)

    setup, oauth and the first run of handler happen at run() or on the first request, not when new Skybridge(...) executes. A duplicate tool name or an invalid oauth.baseUrl therefore rejects the first run() or createServerInstance() call instead of throwing from the constructor. Custom Express middleware registered with app.use() before run() now runs before the bearer check on /mcp; the route itself stays protected.

    McpServer.connectStatelessTransport is removed (PR #1008)

    Per-request instances make it meaningless: the framework connects a transport for every request. Apps that wired their own transport go through app.run() or createServerInstance() (below).

    app.fetchHandler is removed and createServerInstance is async (PR #1008)

    Only relevant if you ran a v2 alpha or beta; neither existed in v1. The HTTP surface an app exposes is the Express listener (app.run() / app.express). Code that dialed app.fetchHandler builds its own handler with createMcpHandler(() => app.createServerInstance()) from @modelcontextprotocol/server. createServerInstance() returns a promise now, since it resolves setup and oauth on first use.

    View tool inputSchema is zod-only (PR #1008)

    Tools registered from a view via useRegisterViewTool take a zod shape (ViewToolInputShape); the bridge validates arguments with z.object() at runtime. Server-side schemas are unaffected and accept any Standard Schema validator.

    RawInputShape and InferSchemaOutput are removed (PR #1008)

    Short-lived aliases from earlier v2 alphas. skybridge/server re-exports the SDK's StandardSchemaWithJSON: use it for a schema and Record<string, StandardSchemaWithJSON> for a shape. For what a schema produces, use your library's own inference helper (z.infer, v.InferOutput, typeof schema.infer).

    SkybridgeServerOptions is what McpServer reads (PR #1008)

    Only relevant if you construct McpServer by hand: its third argument is now { oauth?: boolean; skills?: boolean }. Everything else lives on the Skybridge config.


    Show 13 merged PRs

    Loading…

  2. v1.4View on GitHub →

    Bluebird

    You dropped in, the run went well, and now the clouds have lifted. Bluebird day: the whole face is in front of you, and for the first time you can see your line before you ride it 🌤️

    v1.3.0 was about committing to the run. v1.4.0 is about seeing exactly where you're going. DevTools now renders your app inside a mock ChatGPT or Claude conversation, on desktop and on mobile, so the thing you're building looks like the thing your users will get. And if commerce is your run, there's a full template waiting at the top of the lift.

    • Preview mode: your widget inside a ChatGPT or Claude conversation shell, desktop and mobile, without leaving DevTools
    • Ecom template: npm create skybridge@latest --ecom scaffolds a working shopping app, Shopify included
    • Skills over MCP, updated: skills/list and skills/get per the current SEP-2640, so ChatGPT's skill importer works

    Clear skies, fresh snow. Here's what's new 🏔️


    Preview mode

    Until now, DevTools showed your widget in a panel. Useful, but a panel is not a conversation, and plenty of layout problems only appear once your view sits in a 768px column under a thread of messages.

    Hit preview in the tool panel toolbar and the panel becomes a full ChatGPT conversation, with your real widget in it. Everything around the widget is skeleton (sidebar, thread, disabled composer), measured against the real client in light and dark. Switch the client and you get the same treatment for Claude. Toggle the device and the shell renders inside a 390 x 844 phone frame, with the mobile layout each client actually uses.

    The host context is real too: ui/initialize carries the client's own style variables, container dimensions and available display modes while preview is active, so your view themes itself the way it will in production.

    Two related changes come with it:

    • Display mode is now read-only in the tool panel. It shows what the view asked for, and changes only when the view calls requestDisplayMode, which is how it works in a real client.
    • Fullscreen is bounded to the panel work area, so the tools sidebar and header stay visible while you inspect.

    The DevTools guide covers the whole thing.

    Ecom template

    create-skybridge gains a third template next to demo and blank. Pick ecom in the prompt, or skip it:

    npm create skybridge@latest my-shop -- --ecom
    

    You get a working shopping app: a search-products tool, a render-carousel tool, a product carousel with a detail view, variant picker, image gallery and a small design system, all with Ladle stories so you can work on the components on their own.

    Data goes through a single seam. src/catalog/index.ts re-exports search and getProducts, and that is the only place the app talks to a backend. It ships pointing at a mock catalog, so the app runs before you have any credentials. Point it at ./shopify.js, set SHOPIFY_STORE_DOMAIN and SHOPIFY_STOREFRONT_TOKEN, and you're on the Storefront API. Writing your own provider means exporting those same two functions.

    The ecommerce guide walks through the whole thing.

    Skills over MCP, updated

    v1.3.0 shipped Skills over MCP against the pre-July draft of SEP-2640, which exposed skills as a skill://index.json resource. The SEP has since replaced that index with two mandatory methods, and declaring the extension now commits a server to both.

    Skybridge implements skills/list and skills/get, which unblocks ChatGPT's skill importer: it enters through skills/list and used to get a -32601 back. Nothing changes on your side, keep dropping SKILL.md under src/skills/ with skills: true on the server.

    Still experimental while the SEP is under review.

    Also in this release

    • authplaneProvider: Authplane joins the branded OAuth providers, configuration only, no extra dependencies.
    • useHostInfo: a view can now tell which host it renders in (claude, chatgpt, cursor, goose, …) and adapt copy, shortcuts or layout to it.
    • getToolError: the MCP SDK swallows whatever a tool handler throws, so mcpMiddleware never saw the original Error. getToolError(extra) hands it back with stack and cause intact, which is what you want for Sentry.
    • Verifier messages no longer leak into the WWW-Authenticate challenge unescaped.
    • Asset URL rewriting at runtime now applies to JS only, leaving CSS alone.

    Show 33 merged PRs

    Loading…

    v1.4.1

    Fixes a crash in any tool registered without an inputSchema.

    Show 1 merged PR
    • fix(core): always pass (args, extra) to schema-less tool handlers @harijoe (#1061)
  3. v1.3View on GitHub →

    Drop In

    You're clipped in, the season pass is in your pocket, and the whole mountain is open. Only one thing left to do: point it downhill and drop in 🏂

    v1.2.0 was about where your apps are allowed to ride. v1.3.0 is about committing to the run. You'll be getting an app from your editor to production in one click, teaching the model new tricks, and finally seeing what your view is doing when it's running somewhere you can't open a console.

    • Mixed auth, the easy way: one auth field per tool to enable both public and signed-in tools on the same app
    • Deploy button: ship straight to Alpic from the DevTools, one click, live URL in seconds
    • Skills over MCP: serve Skills from your server as first-class MCP resources
    • View logs in terminal: stream a view's console straight to your dev-server terminal, particularly useful when debugging issues on mobile 📱

    Point your tips down the fall line. Here's what's new 🏔️


    Mixed auth, the easy way

    v1.1.0 gave you mixed auth by hand: mcpAuthMetadataRouter, optionalBearerAuth, and a guard written into every protected handler. v1.2.0 gave you one-line branded providers, but they were all-or-nothing: point a server at an IdP and every tool sat behind sign-in.

    Now the two meet. On any server with an oauth provider, each tool declares its own requirements with a single auth field, and the framework enforces it:

    import { McpServer, descopeProvider } from "skybridge/server";
    import * as z from "zod";
    
    const server = new McpServer(
      { name: "my-app", version: "0.0.1" },
      { capabilities: {} },
      { oauth: await descopeProvider({ url: env.DESCOPE_MCP_SERVER_URL }) },
    );
    
    // Callable signed out — uses the token when one is present.
    server.registerTool(
      {
        name: "search_public_docs",
        description: "Search public documents. No sign-in required.",
        inputSchema: { q: z.string() },
        auth: { allowsAnonymous: true },
      },
      async ({ q }) => ({ content: await search(q) }),
    );
    
    // Requires sign-in with the listed scopes.
    server.registerTool(
      {
        name: "search_private_docs",
        description: "Search the user's private workspace.",
        inputSchema: { q: z.string() },
        auth: { scopes: ["search.read"] },
      },
      async ({ q }, extra) => ({ content: await search(q, { user: extra.authInfo }) }),
    );
    

    See the new auth-descope-mixed example for a full walkthrough.

    Deploy button

    Until now, going from skybridge dev to a live URL meant leaving the DevTools and deploying from the CLI. We've made deploy accessible from the DevTools instead!

    Hit it, sign in, and you're a few seconds away from a live deployed URL.

    Skills over MCP

    Skybridge can now serve Skills straight from your server as skill:// MCP resources. Drop a SKILL.md (plus any supporting files) under src/skills/, flip one option, and the server advertises the skills capability and serves each file over MCP:

    const server = new McpServer(
      { name: "my-app", version: "0.0.1" },
      { capabilities: {} },
      { skills: true },
    );
    

    Experimental while SEP-2640 is under review. Host support for Skills over MCP isn't widely available, as this feature hasn't officially landed in the MCP spec yet. We recommend experimenting with the feature on the Alpic playground, which supports Skills over MCP out of the box.

    View logs in terminal

    When your app runs inside the ChatGPT or Claude desktop or mobile app, there's no console to pop open, so your view's logs have been out of reach. Not anymore.

    Skybridge now wires Vite's forwardConsole into the scaffold, so a view's console prints in the terminal running your dev server. Run through the tunnel, and the logs from an app running on your phone show up on your laptop.

    No more guessing why a view breaks!


    Show 56 merged PRs

    Loading…

    v1.3.5

    Fixes the DevTools OAuth flow against a hosted authorization server (WorkOS AuthKit, Auth0, Clerk, Stytch). DevTools sent its x-forwarded-* headers on the cross-origin discovery, registration and token requests too, so the browser preflighted them and the flow stopped at .well-known/openid-configuration.

    Show 1 merged PR
    • fix(devtools): scope forwarded headers to the MCP server origin @muralx (#1024)
    v1.3.4

    Fixes a build failure for any view whose CSS contains a url() (a @font-face, a background image).

    Show 1 merged PR
    v1.3.3

    Implements skills/list and skills/get per the current SEP-2640 revision, so hosts (starting with ChatGPT's skill importer) can discover skills. The now-removed skill://index.json resource is replaced by these two methods.

    Show 1 merged PR
    • feat(core): implement skills/list and skills/get per current SEP-2640 @qchuchu (#1036)
    v1.3.2

    Fixes a bug with useSendFollowUpMessage and useFiles

    Show 1 merged PR
    v1.3.1

    Adds serverExternal to the Vite plugin, for packages that break the server bundle (e.g. bunyan's optional dtrace-provider).

    Show 1 merged PR
    • fix(build): allow extending esbuild externals via skybridge({ serverExternal }) @harijoe (#1005)
  4. v1.2View on GitHub →

    Season Pass

    Up at the top, the view goes on forever. Ridge after ridge, lift after lift, all of it suddenly within reach thanks to the new season pass 🏔️

    v1.1.0 got apps onto the lift. v1.2.0 is about where they're allowed to ride, this release makes OAuth a first-class citizen, with pre-cut trails to the major identity providers. We've also refreshed the docs so you can explore the whole resort with ease.

    • Branded OAuth providers: WorkOS, Auth0, Clerk, Stytch, and Descope, each wired up around Dynamic Client Registration
    • Refreshed docs: a reworked guide that's easy to navigate
    • One runtime: views now emit a single MCP Apps resource, retiring the separate OpenAI Apps path
    • DevTools as WebMCP tools: the model can now drive the DevTools directly through the browser's WebMCP API

    Branded OAuth providers

    Until now, wiring identity into a Skybridge server meant doing the OAuth legwork by hand. This release makes auth a first-class concern.

    Five branded providers ship ready for Dynamic Client Registration: Auth0, Clerk, Descope, Stytch, and WorkOS. Point one at the IdP and sign-in works out of the box. Auth is now a first-class field on McpServer, so it's declared once and the SDK threads it through the rest of the stack.

    import { McpServer, descopeProvider } from "skybridge/server";
    
    const server = new McpServer(
      { name: "my-app", version: "0.0.1" },
      { capabilities: {} },
      {
        oauth: await descopeProvider({
          // e.g. https://api.descope.com/v1/apps/agentic/{PROJECT_ID}/{MCP_SERVER_ID}
          url: env.DESCOPE_MCP_SERVER_URL, 
        }),
      },
    );
    

    For a setup without a branded provider, customProvider accepts any standards-compliant OAuth authorization server and Skybridge treats it like a built-in one.

    import { McpServer, customProvider } from "skybridge/server";
    
    const server = new McpServer(
      { name: "my-app", version: "0.0.1" },
      { capabilities: {} },
      {
        oauth: await customProvider({
          issuer: "https://issuer.example.com",
          // ...authorization server details
        }),
      },
    );
    

    Paired with the mixed-auth support from the previous release, a server now has full control over access: some tools open to everyone, the rest gated behind whichever provider is configured.

    Refreshed docs

    The docs are organized as a journey. You begin by learning what MCP Apps are, what problem Skybridge solves and how to get started, then move into Build guides that teach each piece of an app in the order you'd actually write it, branch into Guides for cross-cutting topics like auth, files, and UX, and finish in Test and Ship for running and deploying.

    An API Reference sits alongside for exact lookups. Every page reads the same way: it names the problem, shows one complete worked example, then walks through that example piece by piece, so you learn the concept and see the real code at once.

    Explore the new docs

    Unifying to a single runtime

    The OpenAI Apps resource has been retired in favour of the MCP App one. This was made possible by OpenAI's support of MCP Apps: views now always emit a single MCP Apps resource, regardless of host. The change is internal to Skybridge and has no impact on the public API.

    Note: For the change to take effect and stop OpenAI from using the old resource, you'll need to resubmit a new version of your app once you've upgraded to Skybridge 1.2 and deployed the new server to production. The change is backward compatible, so there's no risk of breaking anything in your live traffic.

    DevTools as WebMCP tools

    The DevTools interface is now exposed as WebMCP tools, which means the model can talk to the DevTools the same way it talks to any other view. Inspecting and driving an app during development stops being a manual click-through and becomes something the model can handle on its own.

    Learn more about it in [Code with Fred #7](https://youtu.be/o_csxysfsmw?si=0JU8Yxcj8sgb_b1y)


    Show 18 merged PRs

    Loading…

    v1.2.7

    Add an authorizationServer option to customProvider to advertise the AS independently of the validation issuer, and have descopeProvider pass the agentic AS (not the base-project issuer) through it.

    Show 1 merged PR
    • fix(descope): advertise agentic AS in protected-resource metadata @qchuchu (#950)
    v1.2.6

    Fix descopeProvider to comply with the new issuer format emitted by the Descope platform

    Show 1 merged PR
    • fix(descope): follow discovery issuer for base-project-issuer MCP servers @qchuchu (#942)
    v1.2.5

    Fixes apps using useRequestClose and makes the new conformance app pass

    Show 1 merged PR
    v1.2.4

    This release introduces support for the x-forwarded-prefix header, which is necessary for path-based versioning of deployments on a shared domain.

    Show 1 merged PR
    v1.2.3

    ChatGPT's implementation of sendFollowUpMessage under the MCP protocol leaves a lot to be desired. This patch makes sure that when the OpenAI SDK is available, it keeps using its implementation instead of the MCP one.

    Show 6 merged PRs
    v1.2.2

    Docs/examples refresh with a small ViewName fix and CI release-flow fixes.

    Show 4 merged PRs
    • feat(docs): revamp examples @paulleseute (#897)
    • fix(core): default ViewName to string when registry is empty @harijoe (#898)
    • fix(ci): publish next on main merge and fix release-day version bump @harijoe (#895)
    • feat(generative-ui): add OpenUI generative UI example @vishxrad (#884)
    v1.2.1

    Resolves the legacy widgets/apps-sdk and widgets/ext-apps view URIs to the canonical URI, so that apps already published to the ChatGPT app store keep working after the URI scheme change.

    Show 1 merged PR
    • fix(core): resolve legacy apps-sdk/ext-apps widget view URIs @harijoe (#896)
  5. v1.1View on GitHub →

    First Chair

    Opening day at v1.0.0 was pure chaos, and we loved it!

    A crowd flooded the lift station with their apps, and we handed a ski pass to everyone who showed up. Now the lifts are running and v1.1.0 is ready for the ride 🚠

    This release is about giving your apps more capability and an easier descent!

    • View tools: model can now talk directly to your view instead of just interacting with the server
    • Saved inputs: let you replay your favorite tool calls without climbing the same hill twice
    • Mixed auth: servers let you keep the bunny slope open to everyone while gating the black diamonds behind a sign-in
    • TSDoc: The whole public API is now documented inline, so your IDE reads like a trail map
    • Vercel deploys: the fourth deploy target (yes, that one) even though it cost us a little dignity

    Clip in. Here's what's new on the mountain 🏔️


    View tools

    Until now the traffic was one-way: the model called your server, your server returned results plus a view, and the view just sat there looking pretty. For the model to interact with that view, you had to call tools on the server and either swap in a new view or update the existing one through polling or push.

    With view-provided tools the view can register its own tools. These tools are handlers that run inside the view iframe, against the view's live state, called by the model directly, and without the additional round-trip to the server. You can register tools from your view component using the new useRegisterViewTool hook.

    import { useRegisterViewTool } from "skybridge/web";
    import * as z from "zod";
    
    function Counter() {
      const [count, setCount] = useState(0);
    
      useRegisterViewTool(
        {
          name: "counter_increment",
          description: "Increment the on-screen counter by an amount.",
          inputSchema: { by: z.number().int().default(1) },
        },
        ({ by }) => {
          setCount((c) => c + by);
          return {
            content: [{ type: "text", text: `Counter is now ${count + by}.` }],
            structuredContent: { count: count + by },
          };
        },
      );
    
      return <span>{count}</span>;
    }
    

    The tool registers on mount, unregisters on unmount, and validates arguments against inputSchema before your handler ever runs. View tools are an experimental feature of MCP Apps: Alpic's playground and the MCPJam emulator are the only compatible hosts for now. Have a look at the new chess example for a full demonstration app leveraging this feature.

    Saved input

    Testing an app means calling the same tool with the same arguments over and over. The DevTools now let you save a tool call input and replay it in one click. Dial in the inputs once, hit save, and your favorite calls are waiting for you in the sidebar next session.

    No code required: it's pure DevTools UX. The thing you save is just a tool name plus the arguments you'd otherwise retype by hand.

    <img width="1296" height="720" alt="Clipboard-20260609-150415-701" src="https://github.com/user-attachments/assets/eb9e8c38-b771-4675-bc5e-4796c42e1da1" />

    Pin the inputs you reach for constantly, replay them across sessions, and spend your time reading output instead of refilling in forms.

    Mixed auth

    You used to pick a lane: a fully public app, or a fully authenticated one. Now you can run both on the same server: a few tools open to everyone, the rest gated behind sign-in. Declare each tool's requirements with securitySchemes so hosts can label it before invocation and request the right OAuth scopes, then pair it with the new optionalBearerAuth middleware, which lets unauthenticated requests through instead of shutting everyone out.

    import { optionalBearerAuth } from "skybridge/server";
    import * as z from "zod";
    
    // Anonymous requests pass; bad tokens are still rejected.
    server.use(optionalBearerAuth({ verifier }));
    
    // Open to everyone.
    server.registerTool(
      {
        name: "search_public_docs",
        description: "Search public documents. No sign-in required.",
        inputSchema: { q: z.string() },
        securitySchemes: [{ type: "noauth" }],
      },
      async ({ q }) => ({ content: await search(q) }),
    );
    
    // Requires sign-in.
    server.registerTool(
      {
        name: "search_private_docs",
        description: "Search the user's private workspace.",
        inputSchema: { q: z.string() },
        securitySchemes: [{ type: "oauth2", scopes: ["search.read"] }],
      },
      async ({ q }, extra) => {
        if (!extra.authInfo) {
          return { content: "Sign in required.", isError: true };
        }
        return { content: await search(q, { user: extra.authInfo }) };
      },
    );
    

    The DevTools play along too: a mixed-auth server now offers deferred sign-in, so you can poke around in the public tools first and authenticate only when you reach for a gated one. securitySchemes is client-facing metadata: your handler still enforces auth, but it lets the host label each tool as public or sign-in-required before it is called.

    TSDoc everywhere

    We added TSDoc comments across most of Skybridge's public API. Hover over any hook, any server method, any config field, and the docs you'd otherwise hunt for on the site show up right inside your editor: signatures, parameter notes, links, everything right where you need it. Many of you told us you want to understand the code you depend on without leaving your IDE, and we listened.

    Nothing to install, nothing to configure. Upgrade and your existing imports light up.

    Vercel deploys

    A new deploy target lands, bringing the count to four: Alpic, Cloudflare, Docker, and now Vercel. skybridge build emits a Build Output API tree under .vercel/output/ : a bundled serverless function, the static asset tree, and the routing config. Everything you need for you to ship in one command without vercel.json.

    skybridge build
    vercel deploy --prebuilt
    

    While we may have more affinities for some platforms than others, we believe that diversity and flexibility across the MCP App community win any day. The more places your app can ride, the better.


    Show 8 merged PRs

    Loading…

    v1.1.2

    Patch release

    Skybridge is launching on Product Hunt today 🚀

    To support the launch and help us reach Product of the Day, take a moment to upvote, comment, and leave a review here : https://www.producthunt.com/products/skybridge

    Show 1 merged PR
    v1.1.1

    Patch release

    Explanation

    ChatGPT used to cache the HTML file referenced in a tool resource indefinitely. This recently changed: the cache now lasts about 30min and is then refetched. Before this patch, Skybridge only served the HTML when the exact version hash was provided as a ?v= query param, so resubmitting an app would break view rendering in all earlier conversations once their cache expired and they refetched the old hash.

    Fix

    resources/read now resolves a view by its query-less path, so the underlying asset is served no matter what ?v= value the consumer sends (stale cache key, mismatched version, or no param at all). The version param is only a cache-busting hint and no longer gates resolution. The consumer-facing URI is preserved on the response.

    Show 1 merged PR
  6. v1.0View on GitHub →

    👨‍🍳 We cooked! Skybridge v1 is finally on the table:

    API simplification: We ditched registerWidget for a unified registerTool entry point. In your tool config, simply set the type-safe view.component field, and Skybridge takes care of the rest. We also exposed the underlying Express instance so you can add non-MCP endpoints to your server.

    Devtools revamp: Our beloved MCP inspector gets a radical makeover plus a bunch of new features:

    • Plug your local env to ChatGPT and Claude with one-click HTTP tunneling
    • Chat with your app on the LLM playground
    • Run automated Audit to make sure your server is compliant with OpenAI and Anthropic store guidelines

    Production readiness: We now support Cloudflare Workers, and we added a Dockerfile to make container deployment easy as can be.

    How to migrate?

    1. Install the latest version of the skill
    2. Run the prompt inside your favorite coding agent: Migrate my app based on https://github.com/alpic-ai/skybridge/releases/tag/v1.0.0

    Having issues? Join our Discord to get help from the maintainers.

    Show changes

    Loading…

    v1.0.4

    Enhancements

    Bug fixes

    • fix(cli): increment port instead of random when default is taken (#840) @harijoe
    • fix(capitals): prevent widget crash when requesting a non-capital city (#745) @udaykakade25
    • fix: add tsx to blank template devDependencies (#845) @udaykakade25
    • fix: replace outdated chatgpt-app-builder skill with skybridge (#844) @udaykakade25

    Refactoring

    • refactor(core): wrap user entry to hide setViteManifest from src/server.ts (#832) @harijoe
    v1.0.3

    Enhancements

    Bug fixes

    • fix(core): generate .skybridge/views.d.ts before dev spawns tsc --watch @harijoe (#821)
    • fix(ci): deploy landing on release instead of merge to main @harijoe (#822)
    • fix: relative path import in generative UI example @fredericbarthelet (#819)
    • fix: use 'pnpm run deploy' instead of 'pnpm deploy' in devtools @udaykakade25 (#817)
    • fix(devtools): use 'pnpm run deploy' to avoid built-in pnpm deploy conflict @harijoe (#813)
    • fix: auth0 registration endpoint @paulleseute (#809)

    Testing

    Docs

    • docs(landing): add demo video section between social proof and quotes @harijoe (#810)
    v1.0.2
    Show 6 merged PRs
    v1.0.1
    Show 12 merged PRs