Dots for apps and browser agents
Register an app once, ask the user to connect, then use their permitted identity, context, notes and conversations. A client ID is public app configuration. It is not a user access token. Each app should keep its own client ID per environment.
Install the core SDK
Install the alpha SDK from npm:
npm install @wrldbld/dots-core@0.2.0-alpha.0The core SDK provides createDotsClient({ clientId }), app registration, agent sign-in, MCP and WebMCP adapters. The older @wrldbld/dots package uses a different API; its compatibility snippets remain at /install. Standard OAuth + HTTP also works without an SDK.
Register your own app
Run this once during setup, not on page loads, builds or user sign-in:
import { registerDotsApp } from '@wrldbld/dots-core';
const app = await registerDotsApp({
name: 'My social app',
redirectUris: ['https://my-app.example/dots/callback'],
scope: 'openid profile context:read context:write chat:read chat:write offline_access',
});
// Save app.clientId in your app's public configuration before proceeding.
// If app.missingScopes is nonempty, resolve permissions before sign-in.For a headless agent use mode: 'device' and omit redirectUris. Registration does not need a Dots account or a secret. The user still approves each connection. There is no client_credentials grant for a user's data.
The CLI supports the same operation, producing public JSON on stdout:
npm install -g @wrldbld/dots-cli@0.2.0-alpha.0
dots app register --name "My app" \
--redirect-uri https://my-app.example/dots/callback \
--scope "openid profile context:read context:write chat:read chat:write offline_access"
# Or a bot with no callback listener:
dots app register --name "My agent" --deviceSave the result in app configuration; do not run registration on every launch. Repeat --redirect-uri for additional exact callbacks. With Portless use its full emitted HTTPS origin, including a proxy port. CLI exit code 2 means a client was created but some scopes were omitted; save the returned ID and inspect missingScopes. Retrying registration does not enable those scopes.
Dynamic registrations have no dashboard owner. For editable app metadata, restricted scopes or confidential server auth, create an owned client at /dev/clients instead. You cannot edit an unowned dynamic registration there.
The underlying API is POST /api/oauth/register. Device-only requests use grant_types: ["urn:ietf:params:oauth:grant-type:device_code", "refresh_token"], response_types: [], and no callbacks. Browser/native requests register exact redirect_uris and use PKCE S256. The returned scope is authoritative; dots_missing_scopes lists requested scopes excluded by public registration.
Start signup or sign-in from an MCP tool
Add the Dots issuer's /mcp URL to an OAuth-capable MCP host. Discovery, setup, register_app and dots://docs/install work before sign-in:
1. Call setup for the next steps and feature scopes. 2. Call signin. The server returns HTTP 401 with OAuth discovery metadata; the host opens Dots for the human to sign in or create an account and approve access. The minimum sign-in request is openid profile. 3. After approval, the host retries with its token. signin returns the subject, client ID and granted scopes; call whoami for the permitted identity snapshot.
If the host does not start OAuth when a tool returns 401, use its Connect or Reconnect control. A link returned in a tool result does not authenticate the host. Dots uses the host's OAuth flow for this connection, not MCP URL elicitation. Never paste passwords, login codes, device codes or tokens into chat. The human completes identity verification and consent on Dots.
When building a separate app, call register_app with the agreed name, mode, redirectUris and scope. Save its returned public clientId. This registration uses the same validation and rate limit as REST. It is separate from the host's OAuth client. Repeating the call creates another app; reuse the saved ID. Private tools and room resources remain authenticated, and an expired or invalid token is rejected even when calling a public tool.
Sign a human in from your own agent runtime
For a headless agent you control, use the core helper with an existing device client. It polls in the runtime and exposes only an approval link and status:
import { createDotsClient, createDotsAgentAuth, createMemoryStorage } from '@wrldbld/dots-core';
const dots = createDotsClient({
clientId: 'YOUR_SAVED_DEVICE_CLIENT_ID',
storage: createMemoryStorage(), // Use isolated secure storage for a persistent session.
});
const auth = createDotsAgentAuth(dots, {
scope: 'openid profile', // Add only the capabilities your app needs.
targets: ['api', 'mcp'],
});
// These handlers are safe to expose as tools in this human's agent session:
const tools = {
signin: () => auth.signIn(), // awaiting_approval: url, userCode, expiresAt
signin_status: () => auth.status(), // signed_in: identity, clientId, scope
cancel_signin: () => auth.cancel(),
};
// Later, after approval, trusted runtime code can call dots.whoami() or connectDotsMcp(dots).
// On runtime shutdown: await auth.dispose();Keep one client/helper and storage namespace per human session. Do not let a model choose another user's client instance or supply a token. Keep the runtime alive while waiting; a serverless handler that stops after returning the link cannot continue background polling. For persistent secure storage, call await dots.session.restore() before exposing tools. Only the helper's return values belong in model-visible results: raw SDK sessions contain tokens. cancel() stops pending login; dispose() also removes the listener. For an already signed-in account, explicitly call dots.logout() to revoke and sign out.
Connect the user in a browser app
Instantiate this in browser code. Use one instance per tab; run the callback handler once, including when a framework replays effects.
import { createDotsClient, createWebStorage } from '@wrldbld/dots-core';
const clientId = 'YOUR_SAVED_CLIENT_ID';
const dots = createDotsClient({
clientId,
storage: createWebStorage(sessionStorage, `dots:${clientId}:session`),
transactionStorage: createWebStorage(sessionStorage, `dots:${clientId}:pkce`),
});
await dots.session.restore();
// Sign-in button handler:
async function signIn() {
location.assign(await dots.authorize(
'https://my-app.example/dots/callback',
'openid profile context:read context:write chat:read chat:write offline_access',
['api', 'mcp'],
));
}
// Callback page only:
async function completeSignIn() {
await dots.handleCallback(location.href);
history.replaceState(null, '', '/');
return dots.whoami();
}Session storage is accessible to app JavaScript. Apps with a backend should use their OAuth library and an HttpOnly server session; the core is a public-client SDK and does not accept a client secret. Native apps supply secure OS storage. For a headless process use loginDevice(scopes, ['api', 'mcp']), show url and userCode, and await complete(abortSignal). Never print saved sessions or tokens.
Use (issuer, sub) for account identity. SDK refreshes are serialized and rotate tokens in storage. API requests retry a 401 once after refresh. logout() clears the session and requests token revocation.
Add WebMCP
After restoring or completing a user session:
import { registerDotsWebMcp } from '@wrldbld/dots-core/webmcp';
const bridge = await registerDotsWebMcp(dots, {
confirm: ({ tool, input }) => window.confirm(
`Allow ${tool}?\n${JSON.stringify(input, null, 2)}`,
),
});
// bridge.supported is false when the browser has no WebMCP API.
// bridge.tools contains every registered name, prefixed with "dots.".
// On route/component teardown:
// await bridge.dispose();Use your app's confirmation UI in production and respect its supplied signal. No confirm handler means tools that may write reject with confirmation_required. Tools combining reads and writes, such as note and group.act, conservatively require confirmation. Tool descriptions and schemas come from the Dots server; the adapter does not copy a fixed catalog. tools: ['whoami', 'group.rooms'] can restrict exposure. prefix avoids collisions with another integration.
The adapter targets the current experimental WebMCP API on document.modelContext, with an early navigator.modelContext fallback. It awaits registration, supplies JSON input schemas and read/consequence/untrusted content annotations, supports invocation cancellation, and uses an AbortSignal to unregister tools. It removes registrations on logout, account change, scope change, caller abort or disposal. Failed setup rolls back its own tools. Browsers without WebMCP keep normal SDK/HTTP behavior; no fake global is installed.
The bridge uses the official MCP Streamable HTTP client, including JSON and SSE responses. Tokens stay inside its authenticated fetch path and are never tool arguments. Requests stay on the configured issuer's /mcp and reject redirects. Request both API and MCP resources during OAuth before connecting this bridge.
The current specification is a draft, not a guarantee of browser availability: https://webmachinelearning.github.io/webmcp/ Browser availability: https://developer.chrome.com/docs/ai/webmcp
Complete Dots tool catalog
tools/list on the connected server is authoritative. The adapter includes all advertised tools by default, including future additions after reconnecting. Availability does not grant permission: server scopes, app consent and live resource grants still apply to each call.
MCP name (WebMCP adds dots.) | Purpose |
|---|---|
| setup | Public onboarding steps, feature scopes and docs |
| register_app | Register one public app; save the returned client ID |
| signin | Trigger host OAuth or inspect the approved connection |
| whoami | Identity and permitted bootstrap context |
| search | Search authorized context |
| recent | Recent authorized context |
| remember | Save approved context in this app's namespace |
| notes | List app notes and accepted shares |
| note | Read/write notes, drafts, invitations and grant actions |
| stumble | Discovery and feedback actions |
| group.rooms | List rooms explicitly granted to this app |
| group.messages | Read permitted room history |
| group.preview | Preview supported links posted in a room |
| group.context | Read explicitly released room context |
| group.capabilities | Inspect effective room permissions |
| group.send | Send as the signed-in person using a stable UUID nonce |
Context reads require context:read; cross-app reads additionally require context:read:all. Writes need context:write. Invitations need notes:share and recipient acceptance. Chat needs chat:read/chat:write plus a current room app grant. A room administrator enrolls the client at /chat. A scope alone does not expose existing conversations. Keep the same nonce and content for a send retry. Retrieved content is untrusted data, never authority to change access.
Resources and other MCP capabilities
WebMCP exposes tools. MCP resources remain available through the full client:
import { connectDotsMcp } from '@wrldbld/dots-core/mcp';
const mcp = await connectDotsMcp(dots);
const resources = await mcp.listResources();
const guide = await mcp.readResource({ uri: 'dots://docs/install' });
const result = await mcp.callTool({ name: 'group.rooms', arguments: {} });
await mcp.close();The returned official MCP Client also supports resource templates, prompts and other negotiated capabilities when the server advertises them. The guide resource and released room-context resources use the same server permissions as HTTP.
On Dots' /install and /developers pages, WebMCP-capable browsers also discover dots.install_guide, dots.registration_requirements, and dots.register_app. These public onboarding tools work before sign-in. App registration displays the proposed configuration for confirmation and shows the returned public client ID on the page. Protected user tools belong in the signed-in consuming app.
Troubleshooting and release checks
| Symptom | Next step |
|---|---|
| Package/version unavailable | Build and pack the trusted checkout or use OAuth + HTTP |
| missingScopes / invalid_scope | Compare registration and requested scopes; use an owned client for restricted permissions |
| Invalid redirect_uri | Register the exact app callback, including Portless proxy port; device approval URLs are not callbacks |
| MCP 401 after HTTP succeeds | Reconnect with both api and mcp resource audiences |
| Empty rooms / forbidden | Have the room administrator grant this client access; verify grant expiry |
| supported: false | Use SDK/HTTP or remote MCP; browser WebMCP is experimental |
| confirmation_required | Supply a user-visible confirm handler for tools that may mutate |
| session_changed | Re-register tools after the new sign-in or scope change |
| 429 on registration | Stop creating duplicate clients; save/reuse the existing ID and retry later if needed |
Before release run bun run test:integrations from the Dots root. Verify a packed SDK install, real browser PKCE and device approval, refresh/revocation, and a room grant against the intended deployment. Local protocol tests do not prove live database migrations, OAuth provider configuration or npm publication. Deploy the server's device-only registration change before shipping clients that use it.