Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ jobs:

- uses: pnpm/action-setup@v5

- uses: actions/setup-node@v6
- uses: actions/setup-node@v7
with:
node-version-file: ".nvmrc"
cache: "pnpm"
Expand All @@ -57,7 +57,7 @@ jobs:

- uses: pnpm/action-setup@v5

- uses: actions/setup-node@v6
- uses: actions/setup-node@v7
with:
node-version-file: ".nvmrc"
cache: "pnpm"
Expand Down Expand Up @@ -109,7 +109,7 @@ jobs:

- uses: pnpm/action-setup@v5

- uses: actions/setup-node@v6
- uses: actions/setup-node@v7
with:
node-version-file: ".nvmrc"
cache: "pnpm"
Expand All @@ -135,7 +135,7 @@ jobs:

- uses: pnpm/action-setup@v5

- uses: actions/setup-node@v6
- uses: actions/setup-node@v7
with:
node-version-file: ".nvmrc"
cache: "pnpm"
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/deploy-infra.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:

- uses: pnpm/action-setup@v5

- uses: actions/setup-node@v6
- uses: actions/setup-node@v7
with:
node-version-file: ".nvmrc"
cache: "pnpm"
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/deploy-landing.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ jobs:

- uses: pnpm/action-setup@v5

- uses: actions/setup-node@v6
- uses: actions/setup-node@v7
with:
node-version-file: ".nvmrc"
cache: "pnpm"
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:
steps:
- uses: actions/checkout@v7

- uses: actions/setup-node@v6
- uses: actions/setup-node@v7
with:
node-version-file: ".nvmrc"
registry-url: "https://registry.npmjs.org"
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ Explore all our example templates in the [Examples](https://docs.skybridge.tech/
| <img src="docs/images/showcase-workos.png" alt="Auth WorkOS AuthKit" width="160" /> | WorkOS AuthKit | Full OAuth authentication with WorkOS AuthKit and personalized coffee shop search. | [View code](https://github.com/alpic-ai/skybridge/tree/main/examples/auth-workos) |
| <img src="docs/images/showcase-stytch.png" alt="Auth Stytch" width="160" /> | Stytch | Full OAuth authentication with Stytch and personalized coffee shop search. | [View code](https://github.com/alpic-ai/skybridge/tree/main/examples/auth-stytch) |
| <img src="docs/images/showcase-auth0.png" alt="Auth Auth0" width="160" /> | Auth0 | Full OAuth authentication with Auth0 and personalized coffee shop search. | [View code](https://github.com/alpic-ai/skybridge/tree/main/examples/auth-auth0) |
| <img src="docs/images/showcase-authplane.png" alt="Auth Authplane" width="160" /> | Authplane | Full OAuth authentication with Authplane and personalized coffee shop search. | [View code](https://github.com/alpic-ai/skybridge/tree/main/examples/auth-authplane) |

### UI and component libraries

Expand Down
85 changes: 85 additions & 0 deletions docs/api-reference/authplane-provider.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
---
title: authplaneProvider
sidebarTitle: "Authplane"
description: "Wire OAuth from an Authplane authorization server"
---

`authplaneProvider` wires authentication through [Authplane](https://authplane.ai), so your tools receive a signed-in user.

## Example

```ts server.ts highlight={1,7-10}
import { authplaneProvider, McpServer } from "skybridge/server";

const server = new McpServer(
{ name: "personal-shopper", version: "0.0.1" },
{ capabilities: {} },
{
oauth: await authplaneProvider({
issuer: process.env.AUTHPLANE_ISSUER,
resource: process.env.SERVER_URL,
}),
},
);
```

## Signature

```ts
authplaneProvider(opts: AuthplaneProviderOptions): Promise<OAuthConfig>;
```

## Parameters

### `opts`

- **`issuer`** is the authorization server's issuer identifier, for example `https://auth.acme.com`.

- **`resource`** is this server's resource identifier: the public URL clients reach, advertised in its protected-resource metadata. Required, unlike the other providers — see below.

- **`audience`** overrides the expected `aud`, which defaults to `resource`. Set it only when the resource is configured in Authplane with an explicit audience override.

It also accepts the shared [`CustomProviderOptions`](/api-reference/custom-provider#parameters) options: `serverUrl`, `scopes`, `requiredScopes`, and `metadataOverrides`.

Dynamic Client Registration is supported natively, so clients register directly with Authplane and this server stays out of the authorization path.

## Why `resource` is required

Authplane binds the access token's `aud` to the RFC 8707 resource indicator the client sends, and the client reads that value from the `resource` field of this server's protected-resource metadata. Setting `resource` gives the deployment one fixed identifier for both, so it is required rather than optional.

Three values must therefore be identical, and OAuth compares identifiers by exact string match:

1. the value this server advertises as its `resource` metadata;
2. the resource registered in Authplane;
3. the `aud` Authplane mints, which it takes from (2).

A mismatch between 1 and 2 fails the authorization request with `invalid_target`, before any token exists; between 1 and 3, token verification fails. Register `resource` in Authplane character for character and all three agree.

### Pathless origins

The advertised resource is the URL-normalised form of `resource`, so a bare origin is advertised with a root path: `https://acme.example.com` is advertised as `https://acme.example.com/`. The provider asks for the advertised form up front, and names it if the two differ:

```
authplaneProvider: `resource` must be given in the form it will be advertised.
"https://acme.example.com" is advertised as "https://acme.example.com/".
Use "https://acme.example.com/", or a path-qualified URL such as
"https://acme.example.com/mcp", and register the same value in Authplane.
```

So if your resource is a bare origin, register it in Authplane **with** the trailing slash. Uppercase hosts and explicit default ports normalise the same way. Path-qualified URLs are unchanged by normalisation, and are the most specific identifier available — which is what [RFC 8707 §2](https://www.rfc-editor.org/rfc/rfc8707#section-2) asks clients to send.

## Returns

A `Promise` for the [`OAuthConfig`](/api-reference/custom-provider#returns) you pass to the [`oauth`](/api-reference/mcp-server#constructor) constructor option.

<CardGroup cols={3}>
<Card title="Connect an Identity Provider" icon="fingerprint" href="/guides/auth-providers">
Set up sign-in with a hosted provider
</Card>
<Card title="Authenticate Users" icon="key" href="/build/auth">
Add sign-in to your app end to end
</Card>
<Card title="customProvider" icon="key-round" href="/api-reference/custom-provider">
Wire OAuth from any IdP's discovery document
</Card>
</CardGroup>
1 change: 1 addition & 0 deletions docs/api-reference/overview.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ Every Skybridge export. Server APIs run in your MCP server and work with any hos
| [`useDisplayMode`](/api-reference/use-display-mode) | Read and request inline, pip, or fullscreen. | <Compat compact alpic chatgpt claude goose /> |
| [`useDownload`](/api-reference/use-download) | Save files to the user's device. | <Compat compact claude /> |
| [`useFiles`](/api-reference/use-files) | Upload and pick host-managed files. | <Compat compact chatgpt /> |
| [`useHostInfo`](/api-reference/use-host-info) | Identify which host is rendering the view. | <Compat compact alpic claude cursor goose /> |
| [`useLayout`](/api-reference/use-layout) | Read theme, max height, and safe-area insets. | <Compat compact alpic chatgpt claude cursor goose /> |
| [`useOpenExternal`](/api-reference/use-open-external) | Open a URL outside the view iframe. | <Compat compact alpic chatgpt claude cursor goose /> |
| [`useRegisterViewTool`](/api-reference/use-register-view-tool) | Expose a tool that runs inside the view. | <Compat compact alpic /> |
Expand Down
81 changes: 81 additions & 0 deletions docs/api-reference/use-host-info.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
---
title: useHostInfo
description: "Identify which host is rendering the view"
---

import { Compat } from "/components/compat.jsx";

<Compat alpic claude cursor goose />

The same [view](/build/view) runs inside every host — Claude, Cursor, Goose, and others. `useHostInfo` reports which one, taken from the MCP Apps `ui/initialize` handshake, so the view can adapt copy, shortcuts, or layout to the host it's rendering in. It runs only on MCP Apps hosts; the name is normalized to a [`Host`](#host) slug when recognized, otherwise passed through as a raw string.

## Example

An empty state suggests the next action using the wording that fits the host.

```tsx highlight={4}
import { useHostInfo } from "skybridge/web";

function EmptyState() {
const { name } = useHostInfo();
const hint =
name === "claude"
? "Ask Claude to add your first item."
: "Send a message to add your first item.";

return <p className="empty">{hint}</p>;
}
```

## Returns

### `name`

```tsx
name: Host | (string & {}) | undefined;
```

The host's reported name. It resolves to a [`Host`](#host) slug for recognized hosts, a raw string for hosts not yet mapped, and `undefined` until the handshake completes — the view renders first and re-renders once the host responds. The `(string & {})` keeps the known slugs in autocomplete while still accepting any string.

### `version`

```tsx
version: string | undefined;
```

The host's version string, or `undefined` until the handshake completes.

## Host

The recognized hosts, as normalized slugs. An unrecognized host surfaces its raw reported name instead.

```tsx
type Host =
| "chatgpt"
| "claude"
| "cursor"
| "goose"
| "mistral-vibe"
| "alpic";
```

| Slug | Reported `hostInfo.name` |
| --- | --- |
| `chatgpt` | `chatgpt` |
| `claude` | `Claude` |
| `cursor` | `Cursor` |
| `goose` | `MCP-UI Host` |
| `mistral-vibe` | `Le Chat` |
| `alpic` | `alpic-playground` |

<CardGroup cols={3}>
<Card title="useUser" icon="user" href="/api-reference/use-user">
Read the host's locale and device capabilities
</Card>
<Card title="useMcpAppContext" icon="plug" href="/api-reference/use-mcp-app-context">
Read a raw MCP Apps context value by key
</Card>
<Card title="Design for the Host" icon="sparkles" href="/guides/ux">
Adapt the view to the host it runs in
</Card>
</CardGroup>
3 changes: 3 additions & 0 deletions docs/docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@
"icon": "fingerprint",
"pages": [
"api-reference/auth0-provider",
"api-reference/authplane-provider",
"api-reference/clerk-provider",
"api-reference/descope-provider",
"api-reference/stytch-provider",
Expand All @@ -133,6 +134,7 @@
"api-reference/use-display-mode",
"api-reference/use-download",
"api-reference/use-files",
"api-reference/use-host-info",
"api-reference/use-layout",
"api-reference/use-open-external",
"api-reference/use-register-view-tool",
Expand Down Expand Up @@ -203,6 +205,7 @@
"group": "Auth",
"pages": [
"examples/auth-auth0",
"examples/auth-authplane",
"examples/auth-clerk",
"examples/auth-descope",
"examples/auth-stytch",
Expand Down
22 changes: 22 additions & 0 deletions docs/examples/auth-authplane.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
title: Authplane
description: Full OAuth authentication with Authplane and personalized coffee shop search.
---

import { ChatExample } from "/components/chat-example.jsx";

The Authplane example app demonstrates a full OAuth authentication flow using [Authplane](https://authplane.ai), with a personalized coffee shop finder view that displays user-specific favorites.

<ChatExample
prompt="Find a latte near me."
app="Authplane"
src="/images/showcase-auth.png"
alt="Authplane showcase"
source="https://github.com/alpic-ai/skybridge/tree/main/examples/auth-authplane"
/>

## Skybridge APIs used

- [`authplaneProvider`](/api-reference/authplane-provider)
- [`registerTool`](/api-reference/register-tool)
- [`useToolInfo`](/api-reference/use-tool-info)
22 changes: 21 additions & 1 deletion docs/guides/auth-providers.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ sidebarTitle: "Identity Providers"
icon: "fingerprint"
---

[Authenticating users](/build/auth) wires sign-in through a hosted identity provider in one constructor option, so your tools receive a signed-in user. ChatGPT and Claude drive the flow the same way; what varies is the provider. The sections below cover one each: [Auth0](#auth0), [Clerk](#clerk), [Descope](#descope), [Stytch](#stytch), and [WorkOS](#workos), plus a [custom provider](#any-other-provider) for any other.
[Authenticating users](/build/auth) wires sign-in through a hosted identity provider in one constructor option, so your tools receive a signed-in user. ChatGPT and Claude drive the flow the same way; what varies is the provider. The sections below cover one each: [Auth0](#auth0), [Authplane](#authplane), [Clerk](#clerk), [Descope](#descope), [Stytch](#stytch), and [WorkOS](#workos), plus a [custom provider](#any-other-provider) for any other.

<Info>
These providers require sign-in on every request by default. Set [`auth: { allowsAnonymous: true }`](/api-reference/register-tool#auth) on any tool to [mix public and authenticated tools](/build/auth#mix-public-and-authenticated-tools): the server then serves anonymous requests and Skybridge enforces each tool's own auth declaration before the handler runs.
Expand Down Expand Up @@ -40,6 +40,26 @@ const server = new McpServer(

See the runnable [`auth-auth0`](https://github.com/alpic-ai/skybridge/tree/main/examples/auth-auth0) example.

## Authplane

[Authplane](https://authplane.ai) binds the token's `aud` to this server's resource identifier, so the provider takes both the advertised resource and the expected audience from `resource`. Dynamic Client Registration is supported natively, so clients register with Authplane directly and your server stays out of the authorization path.

1. Deploy or point at an Authplane authorization server and note its URL, e.g. `https://auth.acme.com`.
2. Register this MCP server as a protected resource, using the public URL clients will reach — the same value you pass as `resource`, character for character.

Pass the authorization server URL and this server's public URL to [`authplaneProvider`](/api-reference/authplane-provider):

```ts server.ts highlight={2-5}
const server = new McpServer(serverInfo, capabilities, {
oauth: await authplaneProvider({
issuer: process.env.AUTHPLANE_ISSUER,
resource: process.env.SERVER_URL,
}),
}).registerTool(/* search-products, requires oauth2 */);
```

See the runnable [`auth-authplane`](https://github.com/alpic-ai/skybridge/tree/main/examples/auth-authplane) example.

## Clerk

[Clerk](https://clerk.com/) access tokens carry no `aud` claim, so there is no audience to configure.
Expand Down
Binary file added docs/images/showcase-authplane.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
16 changes: 16 additions & 0 deletions examples/auth-authplane/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Authplane Configuration
# URL of your Authplane authorization server. Dynamic Client Registration is
# supported natively, so no registration proxy is needed.
AUTHPLANE_ISSUER=https://auth.example.com

# Public URL of this MCP server — its resource identifier. Authplane binds the
# token `aud` to the resource indicator the client sends, and the client reads
# that from this server's protected-resource metadata, so it must be the URL
# clients actually reach.
#
# Register this exact string as the resource in Authplane; identifiers are
# compared byte for byte.
SERVER_URL=http://localhost:3000/mcp

# Environment
NODE_ENV=development
Loading