# Logout Source: https://docs.usechar.com/api-reference/auth/logout https://app.usechar.ai/api/spec.json post /auth/logout Revoke the current user's session on WorkOS. This should be called before the client-side signOut() to ensure the server-side session is also invalidated. Returns success: true if the session was successfully revoked. # Get Admin Portal link Source: https://docs.usechar.com/api-reference/directories/get-admin-portal-link https://app.usechar.ai/api/spec.json post /directories/admin-portal Generate a WorkOS Admin Portal link for the organization administrator to configure SSO or Directory Sync. The link expires after 5 minutes. Requires SSO feature to be enabled on the plan. # Get directory Source: https://docs.usechar.com/api-reference/directories/get-directory https://app.usechar.ai/api/spec.json get /directories/{id} Retrieve details of a specific directory connection including synced user and group counts. Returns 404 if the directory does not exist or does not belong to the current organization. # Get directory status Source: https://docs.usechar.com/api-reference/directories/get-directory-status https://app.usechar.ai/api/spec.json get /directories/status Get a summary of the organization's SSO/SCIM connection status. Returns whether directories are connected and active, along with details of the active directory if any. This endpoint is always accessible regardless of plan tier. # List directories Source: https://docs.usechar.com/api-reference/directories/list-directories https://app.usechar.ai/api/spec.json get /directories Retrieve all directory connections (SSO/SCIM) for the current organization. Returns directories with user and group counts. Requires an Enterprise plan with SSO enabled. # List directory groups Source: https://docs.usechar.com/api-reference/directories/list-directory-groups https://app.usechar.ai/api/spec.json get /directories/{id}/groups Retrieve all groups synced from a specific directory. Groups can be mapped to roles in the application for automatic permission assignment. # List directory users Source: https://docs.usechar.com/api-reference/directories/list-directory-users https://app.usechar.ai/api/spec.json get /directories/{id}/users Retrieve all users synced from a specific directory. These are the users provisioned via SCIM from the organization's Identity Provider. # Create end user Source: https://docs.usechar.com/api-reference/end-users/create-end-user https://app.usechar.ai/api/spec.json post /end-users Create a new end user for the current organization. The external_id should be your application's user identifier. End users are typically created automatically when widgets initialize, but you can pre-create them if needed. # Delete end user Source: https://docs.usechar.com/api-reference/end-users/delete-end-user https://app.usechar.ai/api/spec.json delete /end-users/{id} Permanently delete an end user. This removes their record from the system. Note that if they interact with a widget again, a new end user record may be created automatically. # Get end user Source: https://docs.usechar.com/api-reference/end-users/get-end-user https://app.usechar.ai/api/spec.json get /end-users/{id} Retrieve an end user by their unique identifier. Returns user details including external_id, email, and metadata. Returns 404 if the end user does not exist or does not belong to the current organization. # List end users Source: https://docs.usechar.com/api-reference/end-users/list-end-users https://app.usechar.ai/api/spec.json get /end-users Returns all end users for the current organization, ordered by creation date. # Update end user Source: https://docs.usechar.com/api-reference/end-users/update-end-user https://app.usechar.ai/api/spec.json patch /end-users/{id} Update an existing end user. You can change email and metadata. The external_id cannot be changed after creation as it's used for identity mapping. # Error Handling Source: https://docs.usechar.com/api-reference/errors Structured API error responses with HTTP status codes, ORPC error codes, and handling examples in JavaScript and Python The Char API returns structured error responses with conventional HTTP status codes. ## Error Response Format ```json theme={null} { "code": "BAD_REQUEST", "status": 400, "message": "Invalid request data", "data": { "code": "VALIDATION_ERROR", "field": "name", "details": "Name is required" } } ``` * `code` is the ORPC error code (HTTP-aligned) * `data.code` is a machine-readable error identifier * `data` may include additional fields depending on the error type ## Common Error Codes | HTTP | ORPC Code | data.code | Notes | | ---- | ----------------------- | ---------------------- | ----------------------- | | 400 | `BAD_REQUEST` | `VALIDATION_ERROR` | Input validation failed | | 401 | `UNAUTHORIZED` | `AUTH_REQUIRED` | Missing or invalid JWT | | 403 | `FORBIDDEN` | `ACCESS_DENIED` | Permission denied | | 403 | `ORG_CONTEXT_REQUIRED` | `ORG_CONTEXT_REQUIRED` | Org context required | | 403 | `PAYMENT_REQUIRED` | `PAYMENT_REQUIRED` | Subscription required | | 404 | `NOT_FOUND` | `RESOURCE_NOT_FOUND` | Resource missing | | 409 | `CONFLICT` | `DUPLICATE_RESOURCE` | Resource already exists | | 500 | `INTERNAL_SERVER_ERROR` | `INTERNAL_ERROR` | Server error | ## Handling Errors ```javascript theme={null} async function listSkills(token) { const response = await fetch("https://app.usechar.ai/api/organization-skills/list", { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}`, }, body: JSON.stringify({ limit: 20, offset: 0 }), }); if (!response.ok) { const error = await response.json(); throw new Error(`${error.code}: ${error.message}`); } return response.json(); } ``` ```python theme={null} import requests response = requests.post( "https://app.usechar.ai/api/organization-skills/list", headers={ "Authorization": f"Bearer {token}", "Content-Type": "application/json", }, json={"limit": 20, "offset": 0}, ) if response.status_code >= 400: error = response.json() raise Exception(f"{error['code']}: {error['message']}") data = response.json() ``` # Get org GitHub App configuration Source: https://docs.usechar.com/api-reference/github-apps/get-org-github-app-configuration https://app.usechar.ai/api/spec.json get /org-github-apps/config Returns the org's custom GitHub App config if configured, or indicates platform default. # Remove org GitHub App configuration Source: https://docs.usechar.com/api-reference/github-apps/remove-org-github-app-configuration https://app.usechar.ai/api/spec.json delete /org-github-apps/config Removes the custom GitHub App config, reverting to platform default. Admin only. # Start org GitHub App manifest setup Source: https://docs.usechar.com/api-reference/github-apps/start-org-github-app-manifest-setup https://app.usechar.ai/api/spec.json post /org-github-apps/manifest-start Start manifest-based setup for an org-owned GitHub App on github.com. Admin only. # Clear governance provider configuration Source: https://docs.usechar.com/api-reference/governance/clear-governance-provider-configuration https://app.usechar.ai/api/spec.json delete /governance/providers/config Remove the organization-level governance provider configuration (admin only). # Get governance provider configuration Source: https://docs.usechar.com/api-reference/governance/get-governance-provider-configuration https://app.usechar.ai/api/spec.json get /governance/providers/config Get the organization-level governance provider config and effective runtime status. # Test governance provider connection Source: https://docs.usechar.com/api-reference/governance/test-governance-provider-connection https://app.usechar.ai/api/spec.json post /governance/providers/test Test the configured governance provider against the interceptor contract using current form values (admin only). # Upsert governance provider configuration Source: https://docs.usechar.com/api-reference/governance/upsert-governance-provider-configuration https://app.usechar.ai/api/spec.json patch /governance/providers/config Create or update the organization-level governance provider configuration (admin only). # Clear IDP configuration Source: https://docs.usechar.com/api-reference/identity-providers/clear-idp-configuration https://app.usechar.ai/api/spec.json delete /identity-providers/config Remove the IDP configuration for the organization. This will disable SSO-first widget authentication. Only admins can do this. # Get IDP configuration Source: https://docs.usechar.com/api-reference/identity-providers/get-idp-configuration https://app.usechar.ai/api/spec.json get /identity-providers/config Get the current IDP configuration for the organization. Only admins can view this. # Test IDP connection Source: https://docs.usechar.com/api-reference/identity-providers/test-idp-connection https://app.usechar.ai/api/spec.json post /identity-providers/test Test the connection to an identity provider by fetching its OIDC discovery document. Use this to verify the IDP is accessible and properly configured. # Update IDP configuration Source: https://docs.usechar.com/api-reference/identity-providers/update-idp-configuration https://app.usechar.ai/api/spec.json patch /identity-providers/config Update the IDP configuration for widget authentication. Only admins can update this. Set idp_type to configure which identity provider to trust for widget authentication. # OAuth 2.0 Authorization Server Metadata Source: https://docs.usechar.com/api-reference/oauth-discovery/oauth-20-authorization-server-metadata https://app.usechar.ai/api/spec.json get /.well-known/oauth-authorization-server Returns authorization server metadata per RFC 8414. MCP clients use this to discover endpoints for the OAuth 2.0 authorization flow. # OAuth 2.0 Protected Resource Metadata Source: https://docs.usechar.com/api-reference/oauth-discovery/oauth-20-protected-resource-metadata https://app.usechar.ai/api/spec.json get /.well-known/oauth-protected-resource Returns protected resource metadata per RFC 8707. MCP clients use this to discover the authorization server for this resource. # Create organization during OAuth onboarding Source: https://docs.usechar.com/api-reference/oauth/create-organization-during-oauth-onboarding https://app.usechar.ai/api/spec.json post /oauth/create-org Creates a new organization for users who don't have one during the OAuth consent flow. The session must have needsOrgCreation=true. After creation, the session is updated with the new organization context. # Get OAuth client metadata Source: https://docs.usechar.com/api-reference/oauth/get-oauth-client-metadata https://app.usechar.ai/api/spec.json get /oauth/client-info Retrieves client information for display on the OAuth consent page. If the client_id is a URL, fetches metadata from the client's /.well-known/mcp-client endpoint (CIMD). For non-URL client IDs, returns the client_id only. # Get OAuth session info for consent page Source: https://docs.usechar.com/api-reference/oauth/get-oauth-session-info-for-consent-page https://app.usechar.ai/api/spec.json get /oauth/session-info Retrieves user information, CSRF token, and authorized domains for the OAuth consent page. The session ID is obtained from the OAuth callback redirect. # Revoke OAuth token Source: https://docs.usechar.com/api-reference/oauth/revoke-oauth-token https://app.usechar.ai/api/spec.json post /revoke Revokes an access or refresh token per RFC 7009. Returns success even for invalid tokens to prevent token enumeration. # Archive skill Source: https://docs.usechar.com/api-reference/organization-skills/archive-skill https://app.usechar.ai/api/spec.json delete /organization-skills/{skill_id} Soft delete a skill. Archived skills are not visible in listings or available to the AI agent. # Create a new skill Source: https://docs.usechar.com/api-reference/organization-skills/create-a-new-skill https://app.usechar.ai/api/spec.json post /organization-skills Create a new skill from SKILL.md content. Content must include valid YAML frontmatter with name and description. # Get skill by ID Source: https://docs.usechar.com/api-reference/organization-skills/get-skill-by-id https://app.usechar.ai/api/spec.json get /organization-skills/{skill_id} Get a single skill with its full SKILL.md content. # List organization skills Source: https://docs.usechar.com/api-reference/organization-skills/list-organization-skills https://app.usechar.ai/api/spec.json post /organization-skills/list List all skills for the current organization. Returns metadata only for efficient loading. # Update skill with patch Source: https://docs.usechar.com/api-reference/organization-skills/update-skill-with-patch https://app.usechar.ai/api/spec.json patch /organization-skills/{skill_id} Update a skill using patch operations (replace, insert, delete). Patch format follows agent-skills-ts-sdk specification. # Create organization Source: https://docs.usechar.com/api-reference/organizations/create-organization https://app.usechar.ai/api/spec.json post /organizations Create a new organization for the authenticated user. The user becomes an admin of the organization. Use this during onboarding when a user signs in without an organization (the frontend typically redirects to Polar checkout). # Get organization Source: https://docs.usechar.com/api-reference/organizations/get-organization https://app.usechar.ai/api/spec.json get /organizations/{id} Retrieve an organization by its unique identifier. Returns organization details and the current user's role. Returns 404 if the organization does not exist or the user is not a member. # Get organization by slug Source: https://docs.usechar.com/api-reference/organizations/get-organization-by-slug https://app.usechar.ai/api/spec.json get /organizations/by-slug/{slug} Retrieve an organization by its URL-friendly slug. Useful for routing when the slug is in the URL. Returns 404 if the organization does not exist or the user is not a member. # List organization members Source: https://docs.usechar.com/api-reference/organizations/list-organization-members https://app.usechar.ai/api/spec.json get /organizations/current/members Retrieve all members of the current organization with their user profiles and roles. Requires organization context - use this when you need to display team members or manage permissions. # List organizations Source: https://docs.usechar.com/api-reference/organizations/list-organizations https://app.usechar.ai/api/spec.json get /organizations Retrieve all organizations the current user is a member of, including their role in each. Use this to populate organization selectors and determine which organizations the user can access. # API Overview Source: https://docs.usechar.com/api-reference/overview Char REST API for managing organizations, identity providers, skills, threads, usage analytics, and subscriptions The Char API is a REST interface for managing organizations, identity provider configuration, end users, skills, threads, usage, and subscriptions. ## Base URL ## Authentication See [Authentication](/authentication) for details on tokens and org context. ## Resource Groups The OpenAPI spec groups endpoints into the following categories: * **Auth** (session logout) * **Users** and **Organizations** * **Identity Providers** (SSO config + test connection) * **End Users** * **Organization Skills** * **Threads** * **Usage Analytics** * **Directories** * **Plan Tiers** * **Subscriptions** ## Request & Response Format * All requests and responses use JSON. * There is no global envelope; each endpoint returns its own schema. ## Pagination Some list endpoints accept `limit` and `offset` (for example, `POST /organization-skills/list`). Always check the OpenAPI spec for the exact shape and defaults. ## OpenAPI Spec * **Spec**: `https://app.usechar.ai/api/spec.json` * **Docs**: `https://app.usechar.ai/api/docs` # Check feature access Source: https://docs.usechar.com/api-reference/plan-tiers/check-feature-access https://app.usechar.ai/api/spec.json post /plan-tiers/check-feature Check if a specific feature is accessible for the current organization. Returns whether access is allowed and the reason if denied. For limited features, also returns current usage and limit. # Get current plan Source: https://docs.usechar.com/api-reference/plan-tiers/get-current-plan https://app.usechar.ai/api/spec.json get /plan-tiers/current Retrieve the plan tier for the current organization based on their active subscription. Returns the free tier if no active subscription exists. # Get feature summary Source: https://docs.usechar.com/api-reference/plan-tiers/get-feature-summary https://app.usechar.ai/api/spec.json get /plan-tiers/features Retrieve a summary of all features and limits available to the current organization based on their plan tier. Useful for displaying plan info in settings. # Get usage summary Source: https://docs.usechar.com/api-reference/plan-tiers/get-usage-summary https://app.usechar.ai/api/spec.json get /plan-tiers/usage Retrieve a complete usage summary for the current organization including plan features and actual resource usage counts (seats, sites, API keys, end users) compared to their limits. # List plan tiers Source: https://docs.usechar.com/api-reference/plan-tiers/list-plan-tiers https://app.usechar.ai/api/spec.json get /plan-tiers Retrieve all public plan tiers for display on pricing pages. Returns tiers ordered by display order with their features and limits. # Create publishable key Source: https://docs.usechar.com/api-reference/publishable-keys/create-publishable-key https://app.usechar.ai/api/spec.json post /publishable-keys Create a new publishable key for the current organization (admin only). # List publishable keys Source: https://docs.usechar.com/api-reference/publishable-keys/list-publishable-keys https://app.usechar.ai/api/spec.json get /publishable-keys List all publishable keys for the current organization. # Reveal publishable key Source: https://docs.usechar.com/api-reference/publishable-keys/reveal-publishable-key https://app.usechar.ai/api/spec.json post /publishable-keys/reveal Reveal the full value for a publishable key by ID (admin only). # Revoke publishable key Source: https://docs.usechar.com/api-reference/publishable-keys/revoke-publishable-key https://app.usechar.ai/api/spec.json delete /publishable-keys Revoke a publishable key by ID (admin only). Revoked keys stop working immediately. # Check subscription active Source: https://docs.usechar.com/api-reference/subscriptions/check-subscription-active https://app.usechar.ai/api/spec.json get /subscriptions/active Check if the current organization has an active subscription. Returns a simple boolean - use this for feature gating or access control decisions. # Get checkout URL Source: https://docs.usechar.com/api-reference/subscriptions/get-checkout-url https://app.usechar.ai/api/spec.json get /subscriptions/checkout Generate a Polar checkout URL for purchasing or upgrading a subscription. The returned URL includes the organization ID as the customer external ID. Redirect the user to this URL to start the payment flow. # Get customer portal URL Source: https://docs.usechar.com/api-reference/subscriptions/get-customer-portal-url https://app.usechar.ai/api/spec.json get /subscriptions/portal Generate a URL to the Polar customer portal where users can manage their subscription, update payment methods, or cancel. Redirect the user to this URL for self-service subscription management. # Get subscription Source: https://docs.usechar.com/api-reference/subscriptions/get-subscription https://app.usechar.ai/api/spec.json get /subscriptions Retrieve the current organization's subscription details including status, plan, and billing period. Returns null if the organization has no subscription. # Get subscription status Source: https://docs.usechar.com/api-reference/subscriptions/get-subscription-status https://app.usechar.ai/api/spec.json get /subscriptions/status Retrieve a summary of the organization's subscription status. Includes the status, plan ID, period end date, and a boolean indicating whether a subscription record exists. Use this for quick subscription checks in the UI. # Sync subscription from Polar Source: https://docs.usechar.com/api-reference/subscriptions/sync-subscription-from-polar https://app.usechar.ai/api/spec.json post /subscriptions/sync Force sync subscription data from Polar. Fetches the latest state using the organization's external ID and upserts locally. Any local subscriptions not found in Polar are deleted. # Get thread Source: https://docs.usechar.com/api-reference/threads/get-thread https://app.usechar.ai/api/spec.json get /threads/{id} Returns a single thread by ID. Thread must belong to the current organization. # Get thread analytics Source: https://docs.usechar.com/api-reference/threads/get-thread-analytics https://app.usechar.ai/api/spec.json get /threads/analytics Returns aggregated thread statistics for the current organization including total threads, message counts, and status breakdowns. # Get thread messages Source: https://docs.usechar.com/api-reference/threads/get-thread-messages https://app.usechar.ai/api/spec.json get /threads/{thread_id}/messages Returns messages for a thread. Messages are fetched from the ThreadAgent Durable Object. # List all threads Source: https://docs.usechar.com/api-reference/threads/list-all-threads https://app.usechar.ai/api/spec.json get /threads Returns all threads for the current organization, ordered by last message time. # List threads by end user Source: https://docs.usechar.com/api-reference/threads/list-threads-by-end-user https://app.usechar.ai/api/spec.json get /end-users/{end_user_id}/threads Returns threads for a specific end user with pagination. End user must belong to the current organization. # Get daily usage time series Source: https://docs.usechar.com/api-reference/usage-analytics/get-daily-usage-time-series https://app.usechar.ai/api/spec.json get /usage/daily Retrieve daily token usage over time. Useful for charting usage trends. Optionally filter by site. # Get end user usage history Source: https://docs.usechar.com/api-reference/usage-analytics/get-end-user-usage-history https://app.usechar.ai/api/spec.json get /usage/end-users/{end_user_id}/history Retrieve recent usage records for a specific end user. Shows individual chat request details. # Get end user usage stats Source: https://docs.usechar.com/api-reference/usage-analytics/get-end-user-usage-stats https://app.usechar.ai/api/spec.json get /usage/end-users/{end_user_id}/stats Retrieve aggregated token usage statistics for a specific end user. Useful for viewing individual user consumption. # Get organization usage stats Source: https://docs.usechar.com/api-reference/usage-analytics/get-organization-usage-stats https://app.usechar.ai/api/spec.json get /usage/stats Retrieve aggregated token usage statistics for the entire organization. Optionally filter by date range. # Disconnect org script repo Source: https://docs.usechar.com/api-reference/user-scripts/disconnect-org-script-repo https://app.usechar.ai/api/spec.json delete /org-script-repos/current Removes all connected script repo records for the current organization (admin only). # Get org script manifest Source: https://docs.usechar.com/api-reference/user-scripts/get-org-script-manifest https://app.usechar.ai/api/spec.json get /org-script-repos/current/manifest Returns the resolved script manifest for the connected org repository using authenticated org context. # Get org script repo connection Source: https://docs.usechar.com/api-reference/user-scripts/get-org-script-repo-connection https://app.usechar.ai/api/spec.json get /org-script-repos/current Returns the connected GitHub repo metadata for the current org and the install URL to connect one. # Sync org script repo Source: https://docs.usechar.com/api-reference/user-scripts/sync-org-script-repo https://app.usechar.ai/api/spec.json post /org-script-repos/current/sync Refreshes latest SHA from GitHub for the connected repo and updates sync metadata (admin only). # Get current user Source: https://docs.usechar.com/api-reference/users/get-current-user https://app.usechar.ai/api/spec.json get /users/me Retrieve the authenticated user's profile including their organization memberships and roles. This endpoint only requires user authentication, not organization context. Use this to get the current user's identity and available organizations. # Authentication Source: https://docs.usechar.com/authentication Authentication patterns for dashboard APIs and embedded agent ticket exchange Char has two authentication surfaces: dashboard APIs and embedded runtime ticket exchange. ## 1) Dashboard/API authentication Authenticated dashboard and oRPC endpoints use bearer tokens: ```bash theme={null} Authorization: Bearer ``` These tokens are validated server-side and may require org context depending on endpoint. ## 2) Embedded agent ticket exchange Embedded auth uses publishable key ticket exchange: `POST /api/auth/pk-ticket` ```json theme={null} { "publishable_key": "pk_live_...", "id_token": "eyJ...", // optional "embedding_origin": "https://app.example.com" // optional } ``` Response: ```json theme={null} { "ticket": "...", "userId": "user_or_pk_id", "orgId": "org_...", "expiresAt": 1735689600000, "expiresIn": 60 } ``` The web component performs this exchange automatically. Most integrations do not call `/api/auth/pk-ticket` directly. ## Error behavior Common ticket-exchange failures: * `INVALID_BODY` — missing or malformed request body * `INVALID_KEY` — key has invalid format, is not found, or has been revoked * `ORIGIN_NOT_ALLOWED` — request origin does not match the key's `allowed_origins` * Token verification errors (for invalid/expired `id_token`) ## Security best practices * Restrict publishable keys to known origins * Pass `idToken` with `connect()` rather than DOM attributes * Rotate and revoke publishable keys from dashboard settings # Changelog Source: https://docs.usechar.com/changelog/index Release notes, updates, and version history for the Char embedded AI agent platform ## Unreleased * Embed: migrate `@mcp-b/char` to an iframe-based custom-element host contract where `` fills its parent container and syncs host context/responsive layout automatically; React wrappers now live under `@mcp-b/char-react`. * UI: remove the Threads navigation entry and dashboard threads summary to streamline the SaaS UI. * Support: show end-user name/email details and focus the support view on each user’s latest conversation. * Integration: remove the Test tab, keep troubleshooting guidance under OAuth, and make WebMCP tools jump to the relevant tab. * UI: align status, info, and alert colors to semantic tokens for brand consistency across screens. * Embed/Auth (breaking, pre-1.0): unify embed authentication on publishable keys (`publishableKey` required, `idToken` optional), remove `dev-mode` and `ticketAuth`, and replace legacy `clientId`/`organizationId` connect inputs. * API/Auth (breaking, pre-1.0): replace legacy ticket exchange with `POST /api/auth/pk-ticket` (publishable key required, ID token optional) and enforce key-based org resolution with origin checks. * Docs: rewrite Mintlify quickstart, embedding, framework, identity-provider, security, and auth reference pages to document the publishable-key model and new runtime contract. * Docs: promote Vibe Coding to a top-level docs tab with client-first MCP setup patterns (Claude, Codex, Gemini, Cursor, VS Code, Windsurf), MCP styles, and skills/plugin quick links. * Docs: make Claude Code plugin install the primary MCP setup path; plugin install now explicitly auto-configures `chrome-devtools`, `char-docs`, `webmcp-docs`, and `char-saas`. * Docs/UI: reposition governance as integration-based (`LangGuard` or BYO provider), collapse governance navigation to integrations-first, and document trust boundaries (`IdP + provider` authoritative, UI hints non-authoritative). * Security: add approved-client registry cookie support for the MCP OAuth consent flow. * Worker/UI: move PDP governance configuration to org-level settings (`/beta/governance/integrations`) backed by D1 + oRPC, execute policy checks from org provider config, and bypass when no provider is configured or enabled. * MCP: add remote governance management tools (`manage_governance_provider`, `test_governance_provider`) so org governance config can be managed and validated from Char MCP clients. * Embed: add `` in `@mcp-b/char` as the opinionated custom-element shell (iframe-owned PIP composer, desktop push panel, responsive fullscreen policy) while keeping `` as the low-level primitive. * Embed: refactor `@mcp-b/char-react` `CharAgentShell` into a thin wrapper over `` so React and vanilla integrations share the same shell behavior. * Embed: harden `` with fail-closed availability defaults (hide shell fully when iframe init is unavailable) while continuing to emit `char-error` for observability. * Embed: align collapsed PIP composer controls in runtime to shared UI primitives (`Input` and `Button`) for consistent styling and accessibility. * Embed (breaking, pre-1.0): remove legacy `open` host API/bridge compatibility in favor of `displayMode`, and add canonical policy helpers at `@mcp-b/char/display-mode-policy`. * Embed (breaking, pre-1.0): remove deprecated host->iframe `char-styles` and `char-dark-mode` message compatibility; host context is now unified on `char-context` (no change to CSS variables, `connect()`, or `setHostContext()`). * Security (embed): harden host-context ingestion by sanitizing host CSS variable values and enforcing always-on `char-context` schema validation with fail-closed rejection of invalid patches; defer message flood controls, link-domain restrictions, and stricter `style-src` as trusted-host follow-ups. For updates, contact [alex@mcp-b.ai](mailto:alex@mcp-b.ai). # AI-Tested Tools Source: https://docs.usechar.com/explanation/ai-tested-tools Why having the same AI write and test your tools eliminates an entire class of integration problems Most AI tool integrations fail in a predictable way: the tool works when a human uses it, but the AI can't figure out how to use it correctly. This isn't a bug—it's a fundamental mismatch between how humans think about interfaces and how AI models parse them. Char sidesteps this problem by having the same AI that will use your tools also write and test them during development. ## The Impedance Mismatch Problem When a developer writes a tool, they bring assumptions: * "Obviously `user_id` means the numeric ID, not the username" * "The date should be in ISO format, like any reasonable API" * "If the search returns empty, just handle it gracefully" These assumptions aren't documented because they're "obvious." But they're only obvious to humans who share the developer's context. An AI model reading the tool schema doesn't have that context. The result is tools that work perfectly in manual testing but fail mysteriously when an AI tries to use them: * The AI passes a username where an ID was expected * The AI formats the date as "January 15, 2024" instead of "2024-01-15" * The AI interprets an empty response as an error and tries to recover Traditional development catches these issues late—usually when real users report that "the AI isn't working." By then, the tool is deployed, documented, and integrated into workflows. Fixing it requires changes that might break other things. ## The Same-AI Advantage When Claude Code sets up Char tools, it follows a different pattern: 1. **Claude writes a tool** based on your description 2. **Claude immediately calls that tool** through Chrome DevTools MCP 3. **Claude observes what happens** and fixes problems on the spot If Claude can't successfully use the tool it just wrote, it knows immediately. There's no waiting for user reports. The feedback loop is measured in seconds, not days. ```mermaid theme={null} graph LR Write[Write Tool] --> Test[Test via DevTools] Test --> Works{Works?} Works -->|No| Fix[Fix Implementation] Fix --> Test Works -->|Yes| Deploy[Deploy] Deploy --> Use[Embedded Agent Uses Tool] ``` This creates a powerful guarantee: **if Claude could use the tool during development, the embedded agent (also powered by Claude) can use it in production.** ## What Gets Caught The same-AI development loop catches issues that traditional testing misses: **Schema ambiguity.** A parameter named `id` might mean user ID, order ID, or something else. When Claude writes and tests the tool, any ambiguity becomes immediately apparent because Claude will ask for clarification or make the wrong assumption and see the error. **Missing error handling.** If an API returns an unexpected error format, Claude discovers this when testing and adds appropriate handling. Human developers might not hit the edge case in manual testing. **Incomplete descriptions.** Tool descriptions that seem clear to humans might be unclear to models. When Claude tests its own tool and gets confused by the description, it rewrites the description to be clearer. **Type mismatches.** If the schema says `number` but the API actually expects a string, Claude will discover this when the call fails and fix either the schema or the call. **Implicit dependencies.** If a tool only works when called after another tool (e.g., you must be logged in first), Claude discovers this through testing and can document the dependency. ## The WebMCP Standard This workflow is possible because of WebMCP—a standard for exposing tools to AI agents in the browser. Both Chrome DevTools MCP (used during development) and the Char embedded agent (used in production) consume the same WebMCP tool definitions. This means there's no translation layer. Claude doesn't test one version of the tool and then hand off to a different version. The exact tool that passes development testing is the exact tool that runs in production. ```mermaid theme={null} graph TD subgraph Development Claude[Claude Code] -->|Tests via| DevTools[Chrome DevTools MCP] DevTools -->|Reads| WebMCP1[WebMCP Tools] end subgraph Production Agent[Embedded Agent] -->|Uses| WebMCP2[Same WebMCP Tools] end WebMCP1 -.->|Identical| WebMCP2 ``` Contrast this with traditional integrations where you might: * Write tool definitions in one format for development * Transform them to another format for production * Hope the transformation preserves behavior correctly Each transformation is an opportunity for drift. WebMCP eliminates these transformations. ## Why This Matters for Tool Quality Traditional tool development often produces tools that work but are hard for AI to use effectively. Developers optimize for what makes sense to them, not what makes sense to models. When AI tests its own tools, a different optimization pressure emerges. Tools naturally become: **More explicit.** Ambiguous parameters get clarified because the AI couldn't use them otherwise. **Better documented.** Descriptions get refined until the AI can understand them. **More predictable.** Edge cases get handled because the AI encountered them during testing. **More composable.** Tools that are hard to chain together get redesigned because the AI struggled to orchestrate them. This isn't about making tools "AI-friendly" at the expense of human usability. Tools that are clear to AI are generally clearer to humans too. Explicit schemas, thorough documentation, and predictable behavior benefit everyone. ## The Broader Pattern AI-tested tools reflect a broader shift in software development: AI as a first-class participant in the development process, not just an end consumer. Traditional development flow: 1. Human writes code 2. Human tests code 3. Human deploys code 4. AI tries to use code 5. Problems discovered AI-integrated development flow: 1. AI writes code (with human guidance) 2. AI tests code 3. AI and human iterate until it works 4. AI uses code in production The loop is tighter. The feedback is faster. Problems that would have surfaced in production get caught in development. This pattern isn't unique to Char—it's emerging across the AI tooling ecosystem. But Char makes it concrete by using Chrome DevTools MCP as the testing interface and WebMCP as the standard that connects development and production. ## Limitations The same-AI advantage doesn't eliminate all tool problems: **Requirements misunderstanding.** If you describe the wrong requirements, Claude will faithfully implement the wrong thing. AI testing catches implementation bugs, not specification bugs. **Environmental differences.** If your production environment differs from development (different data, different permissions, different scale), issues might not surface until production. **Model capability limits.** Some tasks are genuinely hard for current AI models. If a tool requires capabilities beyond what Claude can do, testing won't reveal this—Claude will just struggle during development too. **Edge case coverage.** AI testing doesn't guarantee comprehensive coverage. Claude tests the paths it thinks of, which might not include every possible edge case. Still, AI testing catches a large class of issues that traditional testing misses entirely. It's not perfect, but it's a significant improvement over the "hope it works" approach. ## Further Reading Set up Chrome DevTools MCP and start the loop The standard that connects development and production Write tools that AI can use effectively Configure your organization from Claude Code # Context Isolation Source: https://docs.usechar.com/explanation/context-isolation How Char agents are isolated from ambient browser state and why this prevents prompt injection Browser automation agents see everything the user sees—the DOM, cookies, localStorage, network requests. This visibility is the source of their power and their danger. Char takes a different approach: the agent sees only what is explicitly provided through defined interfaces. ## What the Agent Sees The agent's context is constructed from four sources: **Conversation.** The messages exchanged between the user and the agent. **Skills.** The SKILL.md files registered by the application, which provide domain-specific instructions and context. **Tool schemas.** The names, descriptions, and input schemas of available tools—but not their implementations or internal state. **Tool outputs.** The results returned when tools are invoked. Comparison diagram illustrating the security difference between unsafe Browser Automation (access to everything) and the safe Char Model (access only to explicit Tool Outputs) That's it. The agent doesn't see the page's DOM, the user's cookies, other users' conversations, hidden form fields, or ambient browser state. Page data becomes accessible only when exposed through a tool, making the boundary explicit and auditable. ## What This Prevents Consider what happens when a browser automation agent visits a malicious page: 1. The page contains hidden instructions in its content 2. The agent reads the page as part of its task 3. The hidden instructions manipulate the agent's behavior 4. The agent performs actions the user didn't intend This attack works because the agent has access to both the malicious content (the page) and privileged capabilities (browser actions, API access). The malicious content hijacks the capabilities. Char's context isolation breaks this chain. The agent can't read arbitrary page content—only tool outputs. If a tool doesn't expose the malicious content, the agent never sees it. If a tool does expose it, the output passes through the guardrails layer before reaching the agent. ## Explicit vs Implicit Context The difference between Char and browser automation agents is the difference between explicit and implicit context. **Implicit context** is ambient state the agent can access without anyone deciding to expose it. A browser agent can read any element on any page it visits. The developer didn't choose to expose that data—it's just there. **Explicit context** requires a deliberate decision. For Char, every piece of information the agent sees was placed there intentionally—by the user typing a message, by the developer registering a skill, or by a tool returning output. This explicitness has consequences: * Developers can reason about what the agent knows * Security teams can audit the boundaries * Users can understand what they're sharing Implicit context makes these properties impossible. You can't audit "everything the agent might read on any page." ## The Trust Boundary Even explicit context can contain problems. A tool might return user-generated content that includes prompt injection attempts. A third-party MCP might return responses designed to manipulate the agent. Char addresses this through trust boundaries rather than content inspection: **Internal tools**—those provided by the embedding application or internal MCP servers—are treated as trusted. The enterprise controls this code and vouches for its behavior. Inspecting internal content would be like distrusting your own database. **External tools**—third-party MCP servers—receive additional scrutiny. The assumption is that external content is potentially hostile. The distinction follows how enterprises already think about trust. Internal systems are assumed safe until proven otherwise. External systems are assumed dangerous until proven safe. Content guardrails (PII detection, prompt injection filtering) are available on the Enterprise plan. The base protection relies on the explicit context boundary itself—if a tool doesn't expose malicious content, the agent never sees it. See [Trust Boundaries](/explanation/trust-boundaries) for how internal and external tools are distinguished. ## Behavioral vs Architectural Defense Security researchers distinguish between behavioral and architectural defenses: **Behavioral defenses** try to prevent bad outcomes by instructing the agent not to do bad things. Prompt engineering, output filtering, and content moderation fall into this category. These defenses can be effective but are inherently probabilistic—a sufficiently clever attack might bypass them. **Architectural defenses** make bad outcomes structurally impossible. If the agent can't see the malicious content, it can't be manipulated by it—regardless of how clever the attack is. Char relies primarily on architectural defenses. Context isolation, identity scoping, and hub-mediated execution are all structural constraints that work regardless of what the model is prompted to do. Guardrails provide an additional behavioral layer for content that must flow through the system, but they're not the primary line of defense. ## Further Reading The overall security design and its rationale How internal and external MCPs are treated differently How the Hub mediates all tool invocations Technical specifications and configuration # Cross-Application Tools Source: https://docs.usechar.com/explanation/cross-app-tools How Char enables AI agents to invoke tools across different applications and why this requires browser-based execution Char's defining capability is cross-application tool access. An agent in your CRM can invoke a tool from your ERP—without either application knowing about the other. This page explains why this is valuable, why it's hard, and how Char makes it possible. ## Why Cross-App Matters Consider a typical enterprise workflow: "Check if Acme Corp's invoice is paid, update their CRM record, and notify their account manager." This touches three systems: ERP, CRM, and email. In a traditional setup, you'd need to: 1. Build an integration between ERP and CRM 2. Build an integration between CRM and email 3. Write orchestration logic to coordinate the workflow 4. Handle errors, retries, and edge cases For one workflow, this might be acceptable. For dozens of workflows, it becomes unsustainable. And for workflows you haven't thought of yet, you'd need to build new integrations before you could even try. AI agents change the equation. Instead of building integrations, you give the agent access to each system and describe what you want. The agent figures out the coordination. But for this to work, the agent needs access to all the systems simultaneously—not just the one you're currently looking at. ## The Technical Challenge Making tools from different applications available to a single agent is harder than it sounds. **Authentication fragmentation.** Each application has its own authentication. Your CRM session is different from your ERP session. An agent that wants to call both needs credentials for both. **Context boundaries.** Traditional web security isolates applications from each other. JavaScript in your CRM can't call APIs in your ERP—same-origin policy prevents it. This isolation is intentional and important for security. **Discovery.** The agent needs to know what tools exist across all applications. If tools are defined per-application, there's no unified view. Most solutions to these problems involve building a server-side proxy. The proxy has service accounts for each application, routes requests between them, and provides a unified tool surface. This works but introduces: * **Credential management.** Service account tokens need to be stored, refreshed, and rotated. * **Over-privileged access.** Service accounts typically have broader permissions than individual users. * **Complex deployment.** The proxy becomes critical infrastructure requiring high availability. * **Audit challenges.** Actions appear to come from the service account, not the user who initiated them. Char takes a different approach. ## Why Browser-Based Execution When you're logged into an application in your browser, you already have an authenticated session. That session represents your identity, your permissions, your access. The session exists—no additional credentials needed. Char's insight is that the browser can be the execution environment. Instead of a proxy with service accounts, tools execute directly in your browser tabs using your existing sessions. ```mermaid theme={null} graph TD subgraph Browser CRM[CRM Tab
Your session] ERP[ERP Tab
Your session] Email[Email Tab
Your session] end Hub[Tool Hub] Agent[AI Agent] CRM <-->|Tools| Hub ERP <-->|Tools| Hub Email <-->|Tools| Hub Agent <-->|Invocations| Hub ``` When the agent calls `erp.lookupInvoice`, the request routes through the [Tool Hub](/explanation/tool-hub) to your ERP tab, which executes it using your ERP session. The API call happens in the browser, uses your cookies, respects your permissions. This means: **No credential storage.** Char doesn't have your ERP password or API token. The browser has your session; that's enough. **Permission parity.** The agent can only do what you can do. If you can't delete invoices, neither can the agent. **Proper attribution.** Your application's audit logs show your user ID, not a service account. **Instant revocation.** When you log out or your session expires, the agent loses access. No tokens to revoke separately. ## The Execution Flow When an agent invokes a cross-app tool, the request traverses several boundaries: ```mermaid theme={null} sequenceDiagram participant Agent as AI Agent participant Hub as Tool Hub participant Frame as Char Agent Frame participant Page as Host Page (ERP) participant API as ERP API Agent->>Hub: Call erp.lookupInvoice({customer: "Acme"}) Hub->>Frame: Forward via WebSocket Frame->>Page: postMessage to host Page->>API: fetch() with session cookie API-->>Page: Invoice data Page-->>Frame: postMessage result Frame-->>Hub: WebSocket response Hub-->>Agent: Tool result ``` Key points: 1. **The Hub routes, doesn't execute.** The Hub never sees the actual API request or response body. It only knows "tool X was called" and passes opaque messages. 2. **postMessage bridges contexts.** The Char agent runs in an iframe with its own origin. Communication with the host page uses `postMessage`, the standard cross-origin messaging API. 3. **fetch() uses the session.** When the host page calls the application's API, the browser automatically attaches session cookies. No code needed to "pass" credentials. 4. **Same-origin policy applies normally.** The host page can only call APIs it could normally reach. Cross-origin requests require CORS headers just like any other JavaScript. ## Tool Namespacing When multiple applications register tools, name collisions are inevitable. Your CRM might have a `search` tool. Your ERP might also have a `search` tool. These do different things. The Hub automatically namespaces tools by their source domain: | Source Domain | Tool Name | In Registry | | ---------------- | --------- | ------------- | | crm.company.com | `search` | `crm.search` | | erp.company.com | `search` | `erp.search` | | docs.company.com | `search` | `docs.search` | The agent sees distinct tools and can choose which to invoke. When you ask "search for Acme in the CRM," the agent knows to call `crm.search`, not `erp.search`. ## Identity Continuity Cross-app tools work because all your applications recognize the same user. Your CRM knows you're Alice. Your ERP knows you're Alice. Char knows you're Alice. This consistent identity is what makes per-user tool aggregation meaningful. This is why Char uses federated authentication. Your identity provider is already the source of truth for who you are across enterprise applications. Char validates tokens from the same IDP, ensuring identity continuity. If your applications use different identity systems—separate user databases with no federation—cross-app tools won't work naturally. The Hub is scoped by user ID, but "user 42 in CRM" and "user 42 in ERP" might be different people. ## Tradeoffs of Browser Execution Browser-based execution has genuine limitations: **Tab dependency.** If you close your ERP tab, ERP tools become unavailable. The Hub reflects reality—you can only invoke tools from applications that are actually open. **Latency.** The message path (agent → Hub → iframe → page → API → page → iframe → Hub → agent) adds round-trips. For interactive use, this is acceptable. For high-frequency automation, it might not be. **Not background-capable by default.** You can't schedule a workflow to run at 3am using your local browser because it isn't open at 3am. For unattended automation, enterprise deployments can use remote browsers with delegated credentials—headless execution that maintains the session inheritance model without requiring user presence. **User presence required.** Someone needs to have the applications open. This aligns with Char's user-centric model—the agent acts on your behalf while you're present—but doesn't fit all use cases. ## Compared to Alternatives **Server-side proxies** (like traditional integration platforms) trade browser dependency for credential management. They can run background workflows but require storing secrets, managing tokens, and often grant broader access than any individual user has. **Browser automation** (like Playwright-based agents) can control any application without predefined tools but can also read arbitrary page content—creating the prompt injection risks Char's tool model avoids. **API aggregators** (like unified APIs) provide a clean interface but require the aggregator to support each application. Adding a new application means waiting for vendor support. Char occupies a middle ground: tool-based access (more secure than automation), browser-based execution (simpler than proxies), but limited to interactive scenarios (unlike background automation). For services that have well-designed APIs—internal microservices, data platforms, custom tooling—remote MCP provides an alternative that doesn't require browser tabs. The Tool Hub aggregates both browser-based and remote tools under the same identity scope. See [Why Char Exists](/explanation/how-char-works) for how these approaches complement each other. ## Further Reading The registry and routing layer Why browser sessions eliminate credential management Security boundaries in tool-based access When you need cross-app vs simpler modes # Deployment Tiers Source: https://docs.usechar.com/explanation/deployment-tiers How Char scales from key-only embed to enterprise governance Char now uses a unified embed auth model across tiers: publishable key required, optional ID token for end-user identity. ## Tier 0: Key-only embed Use a publishable key without per-user ID tokens: ```html theme={null} ``` What you get: * Fastest integration path * Org-scoped runtime and ticket flow * No IDP dependency Tradeoff: * User identity is synthetic (not mapped to your IDP users) ## Tier 1: Key + ID token (recommended production) Pass user identity with `connect()`: ```ts theme={null} agent.connect({ publishableKey: "pk_live_...", idToken, }); ``` What you get: * Per-user identity from your IDP * Cross-app/tooling flows tied to real users * Better auditability and access control alignment ## Tier 2: Enterprise governance Adds governance controls on top of Tier 1: * Policy checks * Approval flows * Audit workflows * Enterprise connectors ## Migration path 1. Start at key-only (`publishable-key`) 2. Add IDP configuration and pass `idToken` 3. Add governance controls as requirements mature All tiers use the same core embed contract: publishable key required, optional ID token for per-user identity. # Enterprise SSO for MCP Source: https://docs.usechar.com/explanation/enterprise-sso-mcp How Char extends enterprise identity to internal MCP tool access Char extends your existing enterprise SSO to internal MCP tool execution. ## Flow overview ```mermaid theme={null} sequenceDiagram participant App as Your Application participant IDP as Your IDP participant Agent as Embedded Agent participant Hub as Tool Hub participant MCP as Internal MCP Server App->>IDP: User authenticates IDP-->>App: ID Token App->>Agent: connect({ publishableKey, idToken }) Agent->>Hub: Authenticated session Hub->>IDP: Token exchange for MCP target IDP-->>Hub: Scoped token/assertion Hub->>MCP: tools/call with scoped credential MCP-->>Hub: Result Hub-->>Agent: Result ``` ## Why this model * User-scoped identity instead of shared service-account credentials * Policy can be enforced centrally at org/IDP boundaries * Access can be revoked through existing identity controls ## Prerequisites * Publishable-key embed integration * IDP configured in Char for `idToken` validation * Remote MCP beta setup for token exchange and connector registration ## Related docs # Federated Authentication Source: https://docs.usechar.com/explanation/federated-auth Why Char delegates authentication to your identity provider and what this means for security Char doesn't manage user accounts. It doesn't store passwords. It doesn't have a user database to breach. Instead, Char *trusts* your existing identity provider—the system that already knows who your users are. This approach is called **federated authentication**: rather than each application maintaining its own identity system, applications trust assertions from a central identity provider. When a user authenticates to your IDP, they receive a cryptographically signed token that proves their identity. Char validates that token rather than asking users to create another account. ## Federated Authentication vs SSO These terms are often confused, but they describe different things: **Single Sign-On (SSO)** means signing in once and accessing multiple applications without re-authenticating. When you log into Google and then access Gmail, Drive, and Calendar without additional login prompts, that's SSO. The "single" refers to the user's experience—one login for multiple apps. **Federated authentication** means trusting identity assertions from an external provider. Your IDP says "this is Alice from Acme Corp" and Char believes it. The "federation" refers to the trust relationship between systems. Char uses federated authentication. Whether your users experience SSO depends on your IDP configuration—if they're already logged into Okta when they visit an application with Char embedded, they won't see a login prompt. But Char itself doesn't implement SSO; it trusts tokens your IDP has already issued. The distinction matters because it clarifies responsibilities. Your IDP handles the authentication experience—login flows, MFA challenges, session management. Char handles authorization within its domain—what tools users can access, what conversations they own. ## How Token Validation Works When a user interacts with the Char agent, your application passes their JWT token—the same token they used to authenticate to your application. Char validates this token against your IDP's public keys. The validation is cryptographic. Your IDP signs tokens with its private key. Char verifies signatures using the corresponding public key, published through the JWKS (JSON Web Key Set) endpoint. If the signature is valid, the token came from your IDP. If it's invalid, the request is rejected. ```mermaid theme={null} sequenceDiagram participant User participant App as Your Application participant IDP as Identity Provider participant Char User->>IDP: Sign in (password, MFA, etc.) IDP-->>User: JWT token (signed) User->>App: Access application App->>App: Store token, establish session User->>Char: Interact with agent (token attached) Char->>IDP: Fetch public keys (JWKS) Char->>Char: Verify token signature Char-->>User: Authenticated, proceed ``` This asymmetric approach has an important property: Char doesn't need any secrets from your IDP. The public key is exactly that—public. Anyone can use it to verify signatures, but only your IDP can create them. **For SSR Applications**: The flow above assumes client-side authentication where the JWT is available in the browser. If your application uses server-side rendering with server-side sessions, see the [SSR Authentication guide](/guides/embedding-agent#ssr-authentication-server-side) for an alternative approach using session tokens. ## Why This Design The federated model eliminates several categories of risk: **Credential theft.** Char has no credential database. There's nothing to steal. Even if Char's servers were compromised, attackers couldn't obtain user passwords because we never had them. **Shared secret exposure.** Traditional integrations require sharing a client secret between services. If either service is breached, the secret is compromised. Char's public key validation has no shared secret—the "secret" (private key) never leaves your IDP. **Shadow access.** When someone leaves your organization, you disable them in your IDP. That's it. Char access is immediately revoked because the user can no longer obtain valid tokens. There's no separate system to update, no orphaned accounts to discover later. **Password policy fragmentation.** Char doesn't enforce password policies because Char doesn't manage passwords. Your IDP enforces whatever policies your organization requires—MFA, password complexity, rotation schedules. Char inherits these protections automatically. ## The Token Lifecycle Tokens are ephemeral. When Char receives a token, the validation process is: 1. Fetch your IDP's current public keys (cached, refreshed periodically) 2. Verify the token's cryptographic signature 3. Check that the issuer (`iss` claim) matches your configured IDP 4. Check that the audience (`aud` claim) includes your application 5. Check that the token hasn't expired (`exp` claim) 6. Extract the user's identity from the subject (`sub` claim) 7. Discard the token There's no token database. Tokens aren't stored, cached long-term, or persisted anywhere that could be breached. Each request validates its token independently. This statelessness has consequences. If your IDP rotates its signing keys, old tokens immediately become invalid—even if they haven't expired. If you revoke a user's access in your IDP, they can't obtain new tokens. Their existing session continues only until their current token expires (typically minutes to hours, depending on your IDP configuration). ## Audience Validation When your IDP serves multiple applications, audience validation prevents token confusion attacks. Consider an organization using Okta for both an internal HR portal and a customer-facing application. A token issued for the HR portal shouldn't grant access to the customer application, even though both use the same IDP. Char enforces this through audience validation. When you configure your organization, you specify your application's client ID. Every token must include this client ID in its audience claim. Tokens intended for other applications—even from the same IDP—are rejected. ```mermaid theme={null} graph LR IDP[Identity Provider] subgraph Tokens T1[Token for HR App
aud: hr-portal] T2[Token for Customer App
aud: customer-app] end subgraph Applications HR[HR Portal
expects aud: hr-portal] Cust[Customer App
expects aud: customer-app] end IDP --> T1 IDP --> T2 T1 -->|✓ Valid| HR T1 -->|✗ Rejected| Cust T2 -->|✗ Rejected| HR T2 -->|✓ Valid| Cust ``` ## How Char Fits Your Trust Model A reasonable question: if tokens are issued for your application, why does Char accept them? Char operates as a **delegated validator** within your application's trust boundary. When you embed Char in your application and configure your client ID, you're telling Char: "validate tokens on my behalf, for my users." This pattern is common in modern architectures: * API gateways validate tokens for backend microservices * Embedded analytics SDKs verify user identity for personalization * Backend-for-frontend (BFF) layers handle auth on behalf of SPAs Char never uses your token to access other services or act as the user elsewhere. It validates the token to establish who the user is, extracts the identity claims, then discards the token. The token's audience is your application—and Char is part of your application's infrastructure. From your IDP's perspective, the trust relationship is with your application. You control which origins can embed Char, and you configure which client ID Char should expect. Char is an implementation detail of how your application handles identity-scoped AI interactions. ## Auth Platforms vs Direct Identity Providers Not all identity providers are the same. There's an important distinction between **direct identity providers** and **auth platforms**. **Direct identity providers** are the source of truth for user identities. Users are provisioned directly in these systems: * Google Workspace — users exist in Google * Microsoft Entra ID (Azure AD) — users exist in Microsoft * Okta (when used as primary IDP) — users exist in Okta **Auth platforms** are intermediaries that handle authentication flows but delegate to upstream providers. They don't store passwords for social logins—they orchestrate OAuth/SAML flows with other providers: * Auth0, WorkOS, Clerk, Scalekit * Better Auth, Logto, Keycloak * Firebase Auth, AWS Cognito When you use an auth platform with social login or enterprise SSO, there's a layered flow: ```mermaid theme={null} sequenceDiagram participant User participant Platform as Auth Platform
(Auth0, Logto, etc.) participant Upstream as Upstream IDP
(Google, Okta, etc.) participant Char User->>Platform: Click "Sign in with Google" Platform->>Upstream: Redirect to Google OAuth User->>Upstream: Authenticate with Google Upstream-->>Platform: Google token + user info Platform->>Platform: Create/update user record Platform-->>User: Platform's JWT (not Google's) User->>Char: Platform's JWT Char->>Platform: Fetch JWKS, validate Char-->>User: Authenticated ``` The key insight: **Char validates the auth platform's tokens, not the upstream provider's tokens.** When a user signs into Auth0 via Google, Auth0 receives Google's token, extracts user information, then issues its own Auth0 token. That Auth0 token is what your application receives and what Char validates. The Google token never reaches Char. This layering has practical implications: **Token claims reflect the platform, not the upstream provider.** The `iss` claim will be `https://acme.auth0.com/`, not `https://accounts.google.com`. The `sub` claim will be Auth0's user ID, not Google's. **The platform is your IDP from Char's perspective.** Configure Char with Auth0's domain/client ID, not Google's. Char doesn't know or care that Google was involved. **User identity is managed by the platform.** If a user signs in with Google today and Microsoft tomorrow (both configured in your Auth0), Auth0 can link these identities. The user has one identity in Auth0, which is what Char sees. **Security policies are enforced by the platform.** MFA requirements, session duration, allowed authentication methods—these are configured in your auth platform, not inherited from Google. This is why the individual [identity provider guides](/identity-providers/index) for Auth0, Logto, Better Auth, etc. all focus on configuring that platform as your IDP—even when users authenticate through upstream providers like Google or enterprise SAML. ## Multi-Tenant IDPs Enterprise identity providers often serve multiple tenants. Azure AD (Entra) serves many organizations through a single infrastructure. Google Workspace distinguishes organizations by domain. Char validates tenant claims to prevent cross-tenant token misuse. A token from one Okta organization can't be used to access another organization's Char deployment, even if both organizations use Okta. The specific claim varies by IDP—`tid` for Azure AD, `hd` for Google Workspace, organization claims for Okta. Char understands these variations and validates appropriately. ## The Tradeoffs The federated model has genuine tradeoffs: **IDP dependency.** If your IDP is down, users can't authenticate to Char. There's no fallback mechanism, no local accounts, no emergency access. Your IDP's availability is Char's availability. **Token lifetime constraints.** Char can't extend token lifetimes. If your IDP issues tokens that expire in 5 minutes, users will need to re-authenticate frequently. Your IDP's token policies apply to Char interactions. **Claim requirements.** Char needs certain claims to function—subject, issuer, audience, expiration. If your IDP doesn't include these (unusual but possible with custom configurations), integration won't work. These tradeoffs are intentional. The alternative—maintaining our own credential database, supporting local accounts, extending token lifetimes—would reintroduce exactly the risks the federated model eliminates. ## Remote MCP Servers (Private Beta) The federated model can extend beyond user authentication to internal MCP servers. This feature—currently in private beta—uses your same IDP to obtain scoped tokens for internal services, so the user's identity flows through the entire tool chain. **Private Beta:** Remote MCP with enterprise SSO requires additional IDP setup. See [Enterprise SSO for MCP](/explanation/enterprise-sso-mcp) in the Private Beta section for details. ## What Your IDP Already Handles By delegating to your IDP, Char inherits capabilities that would otherwise require significant implementation: | Capability | Handled By | | -------------------------------- | ---------- | | Password storage and hashing | Your IDP | | Multi-factor authentication | Your IDP | | Brute force protection | Your IDP | | Session management | Your IDP | | Password reset flows | Your IDP | | Account lockout policies | Your IDP | | Compliance logging | Your IDP | | User provisioning/deprovisioning | Your IDP | Char doesn't need to implement any of this because your IDP already does. ## Further Reading The overall security design and its rationale Configure your specific IDP How authentication establishes trust for internal tools Remote MCP with identity delegation (Private Beta) # Why Char Exists Source: https://docs.usechar.com/explanation/how-char-works The problem Char solves and why its architecture takes the form it does AI agents need access to your applications to be useful. But giving an AI access to your systems is dangerous. Char exists to resolve this tension—enabling powerful cross-application AI workflows while maintaining the security properties enterprises require. ## The Integration Problem Before AI agents, connecting applications required building point-to-point integrations. Want your CRM to know about invoices in your ERP? Build an integration. Add a ticketing system to the mix? Build two more integrations. The work scales quadratically with the number of applications. AI agents promised to solve this. Instead of building rigid integrations, you'd give the agent access to each application and let it figure out the workflows. "Find the invoice for Acme and add the status to their CRM record" becomes a natural language request instead of a custom integration project. But this promise came with a problem: how do you give an agent access to your applications without giving it the keys to the kingdom? ## The Security Problem Most AI agents that interact with applications fall into one of two categories: **Server-side agents** run on infrastructure you control, making API calls with service accounts. They're secure in the sense that you control the code, but they require building OAuth integrations, managing credentials, and granting permissions that often exceed what any individual user has. When something goes wrong, the blast radius is the entire service account's access. **Browser automation agents** run in the user's browser, inheriting their session. They can do anything the user can do—which sounds right until you realize they can also *see* anything the user can see. A malicious webpage can embed instructions that hijack the agent's behavior. Security researchers call this "prompt injection," and it's the defining vulnerability of browser-based AI. Char takes a third path. ## Three Kinds of Tool Sources The integration problem isn't monolithic. Different applications present different challenges, and Char addresses them with three complementary approaches. ### WebMCP: When the UI Is the Interface Many enterprise applications don't have public APIs. Or they have APIs that are incomplete, poorly documented, or require expensive licensing tiers. What they do have is a user interface—often one that represents decades of refinement. This isn't accidental. UIs encode hard-won lessons about what users actually need. The placement of buttons, the flow of forms, the structure of dashboards—all of this reflects years of feedback and iteration. The UI *is* the interface, in the deepest sense. WebMCP tools execute in the browser, operating through the same UI the user sees. When you expose a "search customers" tool, you're not recreating API logic from scratch—you're leveraging the search functionality the application already provides, with all its edge case handling and business logic intact. The approach has a profound security benefit: session inheritance. The tool uses the user's existing browser session. No OAuth flows, no stored credentials, no service accounts. When the user logs out, the tool stops working. Permissions are exactly what the user has—no more, no less. The tradeoff is presence. The user needs the application open in a browser tab. This is ideal for interactive AI assistance where the user is already working with the application. For unattended automation, enterprise deployments can use remote browsers—headless execution with delegated credentials that maintains session inheritance without requiring user presence. ### Remote MCP: When APIs Exist Not every application lacks an API. Many internal services—microservices, data platforms, custom tooling—expose well-designed programmatic interfaces. For these, browser-based execution would be unnecessarily indirect. Remote MCP servers connect directly to the Tool Hub over HTTP. They're like API endpoints, but with two crucial advantages. **No versioning headaches.** Traditional APIs require careful versioning—breaking changes require coordination across all consumers. MCP's tool model is more flexible. Tools declare their schemas, and the AI adapts. Add a parameter, and the AI learns to use it. Change a response format, and the AI adjusts. The human-in-the-loop nature of AI workflows absorbs variations that would break rigid integrations. **Authentication built in.** When an internal MCP server shares your enterprise identity provider, the Tool Hub can authenticate using the same user identity that's already established. No service accounts, no separate credential stores, no orphaned access when someone leaves. The user's IDP token flows through the Hub to the remote server—same identity, same permissions, same audit trail. This is particularly powerful for internal services. Your enterprise already has an IDP. Your internal MCP servers can trust it. The Tool Hub becomes the bridge that carries identity across service boundaries without credential sprawl. ### External MCP: Third-Party Services Some tools come from outside your enterprise—SaaS applications, vendor APIs, public services. These require a different trust model. External MCP servers authenticate through their own mechanisms, typically OAuth with user consent. The user explicitly authorizes the connection, and the Tool Hub manages the resulting tokens. This is closer to traditional integration patterns, but unified under the same tool surface. The distinction matters for security. Internal tools—those authenticated through your enterprise IDP—are treated as trusted. Your enterprise controls the code, vouches for its behavior. External tools receive additional scrutiny because you don't control what they return. This trust boundary isn't arbitrary. It reflects how enterprises already think about security: internal systems are trusted until proven otherwise, external systems are verified before trust is extended. ## Why Tools Instead of Automation All three approaches share a common philosophy: agents operate through defined interfaces, not ambient capabilities. When an agent has ambient capabilities—like reading any DOM element or calling any API—you can't reason about what it might access. The attack surface is unbounded and unauditable. You're trusting the agent's judgment about what to look at and what to ignore. When an agent operates through tools, you can enumerate exactly what it can do. Each tool has a defined schema, a documented purpose, and a clear boundary. Security teams can audit the tools. Developers can reason about the data flows. Users can understand what they're authorizing. This is similar to how modern operating systems handle permissions. Early systems gave programs ambient access to everything. Modern systems require explicit permission grants—this app can access your location, this app can read your contacts. The tool model applies this principle to AI agents. ## The Hub Architecture Given that agents should operate through tools, the next question is: how do tools from different sources work together? The naive approach is to have each application maintain its own tool registry. The agent in your CRM knows about CRM tools. The agent in your ERP knows about ERP tools. Cross-application workflows require copying data between them manually. Char introduces the Tool Hub—a per-user registry that aggregates tools from all sources. Browser tabs register their WebMCP tools. Remote MCP servers register their tools. External services register theirs. All converge in a single, identity-scoped registry. ```mermaid theme={null} graph TD subgraph Browser["Your Browser"] CRM[CRM Tab
WebMCP Tools] ERP[ERP Tab
WebMCP Tools] end subgraph Internal["Internal Services"] DataAPI[Data Platform
Remote MCP] Ticketing[Ticketing System
Remote MCP] end subgraph External["External Services"] Calendar[Calendar
External MCP] end Hub[Your Tool Hub
Identity-Scoped] CRM -->|Register| Hub ERP -->|Register| Hub DataAPI -->|Register| Hub Ticketing -->|Register| Hub Calendar -->|Register| Hub Agent[AI Agent] <-->|Invoke any tool| Hub ``` This creates something previously impossible: **unified tool access across browser sessions, internal services, and external applications**. The agent can query your internal data platform, update your CRM, and schedule a calendar event—all in one workflow, all under one identity. The Hub is scoped to you specifically—your identity, your permissions, your tool registry. There's no shared state between users. This scoping is enforced at the infrastructure level using Cloudflare Durable Objects, which means cross-user access isn't just prohibited by policy—it's structurally impossible. ## The Identity Foundation Everything in Char flows from identity. Your Tool Hub is scoped to your user ID. Browser tools execute with your session. Remote MCP calls carry your identity. Conversations are stored under your account. Cross-application workflows are possible because all your tools recognize the same user. This is why Char delegates authentication to your existing identity provider rather than maintaining its own user database. Your IDP is already the source of truth for who you are across your enterprise applications. Char validates tokens your IDP issues rather than creating a parallel identity system. For browser-based tools, identity enables session inheritance—the tool uses your existing session, your existing permissions. For remote MCP, identity enables token exchange—the Hub obtains appropriately-scoped credentials for each service, all rooted in your original IDP token. For external services, identity enables consent management—you authorize connections, and the Hub maintains them on your behalf. The design has implications: * **No credential storage.** Char never sees your password. Browser sessions stay in the browser. Remote tokens are scoped and short-lived. * **Centralized revocation.** Disable a user in your IDP and they immediately lose access to everything—browser tools, remote MCP, external services. * **Inherited policies.** Whatever authentication policies your IDP enforces—MFA, password complexity, session timeouts—apply to Char automatically. ## What This Enables The combination of multiple tool sources, hub aggregation, and unified identity enables workflows that were previously impractical: **Cross-application AI.** An agent can orchestrate actions across browser-based applications, internal services, and external SaaS—all in natural language. The integrations that would have taken months to build become conversational requests. **Permission-appropriate access.** Whether the tool executes in a browser tab or calls a remote API, it operates with your permissions—not a service account's elevated access, not another user's data. The blast radius of any mistake is limited to what you could have done manually. **Auditable boundaries.** Security teams can review what tools exist across all sources, what data they expose, what actions they enable. The attack surface is enumerable rather than ambient. **Incremental adoption.** Start with browser-based tools for applications that have UIs. Add remote MCP for internal services with APIs. Connect external services as needed. Each addition expands capability without requiring rearchitecture. **Legacy application access.** Applications that will never have APIs—or whose APIs would take years to build—become accessible through their existing UIs. The decades of business logic encoded in those interfaces becomes available to AI workflows. ## Further Reading The aggregation architecture in depth How agents are isolated from browser state Why browser-based execution works How identity delegation works Internal MCP access via XAA/ID-JAG (Private Beta) Technical specification for ID-JAG (Private Beta) # Organizational Learning Source: https://docs.usechar.com/explanation/organizational-learning How Char captures tribal knowledge and decision traces, turning ephemeral expertise into institutional memory Every organization runs on knowledge that isn't written down. The senior engineer who knows why that API is structured oddly. The support lead who remembers the exception they made for a key customer last quarter. The operations manager who understands which approvals are really required versus which are rubber stamps. This knowledge lives in people's heads, materializes in Slack threads and hallway conversations, and evaporates when people leave. It's the difference between what the documentation says and how things actually work. AI agents bump into this wall constantly. They have access to tools. They can read your APIs and databases. But they don't know that healthcare customers always get an extra 10% because their procurement cycles are brutal. They don't know that the "required" security review is actually optional for internal tools. They don't know any of the accumulated wisdom that makes your organization function. Char is designed to capture this knowledge over time. ## The Evolution: Tools, Skills, Context The progression from basic AI capabilities to genuine organizational intelligence follows three phases: **Tools give agents capability.** Through MCP, agents can interact with your systems—query databases, call APIs, execute actions. But an agent with tools is like a new hire with system access. They can technically do things, but they don't know how your organization actually operates. **Skills give agents expertise.** A [skill](/explanation/skills) is procedural knowledge—the "how to" that teaches an agent to use tools effectively. Skills capture the workflows, the edge cases, the decision trees. An agent with skills is like an employee who's read all the SOPs. **Context gives agents experience.** This is the accumulated record of decisions, exceptions, and outcomes. Not just what the procedure says, but what happened last time. What exceptions were granted and why. What worked and what didn't. An agent with context is like an employee who's been there for years and remembers everything. | Phase | What the Agent Has | Analogy | | ------- | ------------------ | -------------------------------- | | Tools | Capabilities | New hire with system access | | Skills | Procedures | Employee who's read the SOPs | | Context | Experience | Veteran who remembers everything | Most AI deployments stop at tools, maybe skills. Char is architected to reach context. ## Why Decisions Disappear Traditional systems of record—your CRM, ERP, ticketing system—capture outcomes. The deal closed at this price. The ticket was escalated to Tier 3. The refund was approved. What they don't capture is the reasoning. Why that price? Who approved the deviation? What precedent did they cite? What context informed the escalation? This reasoning exists, briefly, in the moment of decision. It lives in the conversation that preceded the action, in the judgment call someone made, in the memory of similar situations. Then it evaporates. The system records the final state, not the path that led there. When an AI agent executes a workflow, the same thing happens. The agent makes decisions—which tool to call, what parameters to use, how to handle an edge case. These decisions are informed by context, but that context disappears when the task completes. The next time a similar situation arises, the agent starts from scratch. This is the wall that prevents AI from accumulating genuine organizational intelligence. ## How Char Captures Context Char's architecture is designed to make decisions durable rather than ephemeral. **The Hub sees everything.** Every tool invocation—whether it's a browser-based WebMCP tool, an internal MCP server, or an external service—flows through the user's Tool Hub. The Hub doesn't just route requests; it can observe patterns, log decisions, and build a record of what happened and why. **Skills are living documents.** Unlike static documentation, skills can be created and refined through conversation. When a user describes a workflow, the agent can capture it as a skill. When an exception occurs, the skill can be updated to reflect the new precedent. The knowledge base grows through natural interaction. **Execution becomes precedent.** When the agent handles a situation, that handling becomes a reference point. "Last time we saw this pattern, here's what we did." Over time, the agent doesn't just follow procedures—it remembers outcomes. ```mermaid theme={null} graph TD subgraph Today["What Happens Today"] T1[Agent executes workflow] T2[Decision made in context] T3[Outcome recorded in system] T4[Reasoning lost] end subgraph Tomorrow["What Char Enables"] C1[Agent executes workflow] C2[Decision traced through Hub] C3[Outcome recorded in system] C4[Reasoning captured as context] C5[Future decisions informed by precedent] end T1 --> T2 --> T3 --> T4 C1 --> C2 --> C3 --> C4 --> C5 C5 -.->|"Informs"| C2 ``` ## Skills as the Bridge Skills occupy a unique position in this architecture. They're explicit enough to be auditable, flexible enough to evolve, and structured enough to be machine-readable. A skill isn't just instructions—it's a boundary around a domain of knowledge. When a user asks something that matches a skill's description, the agent loads that skill's full context. This progressive disclosure means the agent can have access to vast procedural knowledge without being overwhelmed by irrelevant detail. The [AgentSkills specification](https://agentskills.io) defines skills as folders containing a `SKILL.md` file with metadata and instructions. This simple format has profound implications: **Skills are versionable.** Store them in git. Review changes in pull requests. Track who modified what and when. This matters for regulated industries where audit trails are required. **Skills are portable.** A skill written for Char works in Claude Code, Cursor, and other compatible tools. You're not locked into a proprietary format. **Skills are refinable.** When the agent encounters an edge case, the skill can be updated. When a better approach is discovered, the instructions can change. Skills improve through use. Most importantly, skills can be created from experience. The agent observes a workflow, captures it as a skill, and that knowledge becomes available for future use. Tribal knowledge becomes institutional knowledge. ## The Decision Trace A decision trace captures not just what happened, but the context that informed it: * What was the user trying to accomplish? * What tools were available? * What skills were loaded? * What precedents were considered? * What decision was made and why? * What was the outcome? This is richer than a log. A log tells you that a tool was called with certain parameters. A decision trace tells you why that tool was chosen, what alternatives were considered, and how the situation was understood. On the Enterprise plan, Char captures decision traces for compliance and audit purposes. But the deeper value isn't compliance—it's organizational learning. Every traced decision becomes a potential precedent. Every exception becomes a data point for refining procedures. ## From Individual to Organizational The vision extends beyond individual users. When Char is embedded across an organization's applications, patterns emerge: * How do different teams handle similar situations? * What exceptions are being granted repeatedly? (Maybe the exception should become the rule.) * What workflows are being invented ad-hoc? (Maybe they should become skills.) * Where are decisions inconsistent? (Maybe alignment is needed.) This is the context graph—the accumulated record of how your organization actually operates, as opposed to how the documentation says it should operate. It's the difference between the org chart and how decisions really get made. Char doesn't capture this today in its full form. But the architecture—Hub-mediated execution, enterprise audit logging, refinable skills—is designed to make it possible. ## The Tradeoffs This vision involves real considerations: **Privacy and consent.** Capturing decision traces means recording what people do. Organizations need clear policies about what's captured, who can access it, and how it's used. The enterprise controls are designed to give organizations this control. **Signal versus noise.** Not every decision is worth tracing. A search query has different value than an exception approval. The system needs to distinguish routine operations from meaningful precedents. **Interpretation.** A decision trace records what happened, but interpreting why requires judgment. Two similar situations might warrant different decisions for reasons that aren't captured in the trace. **Cold start.** An organization deploying Char today doesn't have accumulated context. The value builds over time through use. Early adopters are investing in future capability. These tradeoffs are inherent to any system that attempts to capture organizational knowledge. They're not reasons to avoid the attempt—they're considerations for how to do it responsibly. ## Further Reading How skills encode procedural knowledge The architecture that enables context capture Enterprise controls for audit and compliance Write skills that capture your workflows # Security Architecture Source: https://docs.usechar.com/explanation/security-architecture How Char's security model works and why it's designed this way AI agents need access to tools and data to be useful, but that same access creates risk. This page explains why Char's security architecture takes the form it does—the threats it addresses, the constraints it imposes, and the tradeoffs it accepts. ## The Threat Model Most browser automation tools give AI agents ambient access to the user's browsing context—cookies, sessions, DOM content, network requests. This creates what security researcher Simon Willison calls the "lethal trifecta": ```mermaid theme={null} graph TD subgraph Danger["Danger Zone"] PI[Prompt Injection Attack] end PD[Access to
Private Data] --> PI UC[Exposure to
Untrusted Content] --> PI EX[Ability to
Exfiltrate] --> PI style Danger fill:#ff6b6b,stroke:#cc5555,color:#fff style PD fill:#ffd93d,stroke:#e6c235 style UC fill:#ffd93d,stroke:#e6c235 style EX fill:#ffd93d,stroke:#e6c235 ``` When all three conditions exist, an attacker can embed malicious instructions in content the agent reads (a web page, an email, a document), causing it to leak private data or perform unauthorized actions. The attack surface isn't a bug—it's inherent to the architecture. Char addresses this threat model by removing these conditions structurally rather than trying to filter them behaviorally. ## The Design Principles Char's security architecture follows three principles: ### 1. No Credential Storage Char doesn't store user passwords, session tokens, or shared secrets. Authentication is delegated entirely to your identity provider (Okta, Azure AD, Auth0, Google, etc.). When a user interacts with the agent, your application passes their existing JWT token. Char validates the token signature against your IDP's public keys, extracts the user identity, and discards the token. There's no token database to breach. This has implications: * **Centralized revocation.** Disable a user in your IDP and their Char access ends immediately. No separate system to update. * **No shared secrets.** Char validates signatures using public key cryptography. We never see your IDP's client secret. * **Inherited security.** Your IDP's MFA, password policies, and brute-force protection apply automatically. Char doesn't implement these—it trusts your IDP already does. The tradeoff is dependency. If your IDP is unavailable, users can't authenticate to Char. See [Federated Authentication](/explanation/federated-auth) for why this is different from SSO and how token validation works in detail. ### 2. Infrastructure-Level Isolation Every user gets their own isolated execution context—not through application-level access control (`WHERE user_id = ?`), but through infrastructure-level isolation using Cloudflare Durable Objects. ```mermaid theme={null} graph TD subgraph Users["Different Users, Same Organization"] U1[Alice] U2[Bob] end subgraph Infra["Infrastructure-Level Isolation"] DO1[Alice's
Durable Object] DO2[Bob's
Durable Object] end U1 --> DO1 U2 --> DO2 DO1 -.->|"No code path exists"| DO2 style DO1 fill:#41d1ff,stroke:#2bb8e6 style DO2 fill:#41d1ff,stroke:#2bb8e6 ``` The distinction matters. Application-level isolation relies on every query having the correct filter. If a developer forgets a `WHERE` clause or writes a bug, data can leak. Infrastructure-level isolation means there's literally no code path that could return another user's data—each user's state exists in a separate execution context. Each Durable Object maintains: * The user's active tool connections (which browser tabs are connected) * Their tool registry (what tools are currently available) * Their conversation threads (message history) Cross-user access isn't prevented by policy—it's prevented by there being no shared state to access. See [Tool Hub](/explanation/tool-hub) for why Durable Objects were chosen over alternatives like Redis or traditional databases. ### 3. Explicit Context Only The agent sees only what is explicitly provided through defined interfaces: | Context Source | What the Agent Sees | | -------------- | ------------------------------------------- | | Conversation | Messages typed by the user | | Skills | SKILL.md files your developers registered | | Tool schemas | Names and input schemas—not implementations | | Tool outputs | Results returned when tools execute | The agent does **not** see: * The page's DOM * Cookies or localStorage * Other users' data * Ambient browser state * Anything not explicitly passed through a tool This explicitness breaks the prompt injection attack chain. If a malicious page contains hidden instructions, the agent never sees them—unless a tool explicitly extracts and returns that content. And if a tool does return it, the content passes through defined boundaries that can be logged and audited. See [Context Isolation](/explanation/context-isolation) for how this compares to browser automation approaches and why it prevents prompt injection. ## The Trust Model Not all tools are equally trusted. Char distinguishes between internal and external tools based on who controls them. **Internal tools** are part of your enterprise—MCP servers you operate, browser-based tools in your applications. The reasoning: if an internal tool misbehaves, that's either a bug in your code or a compromised internal system. Both are your enterprise's responsibility to address. Char treating your internal tools as hostile would be like your database distrusting your own API. **External tools** come from third parties—SaaS applications, public APIs, vendor MCP servers. Your enterprise doesn't control this code, so Char applies more scrutiny. External tool responses pass through validation before reaching the agent. The boundary is defined by your identity infrastructure. Tools that authenticate via your IDP are internal. Everything else is external. See [Trust Boundaries](/explanation/trust-boundaries) for how this trust model is enforced and why internal tools aren't validated the same way. ## How Requests Flow Every tool invocation flows through a consistent path: ```mermaid theme={null} sequenceDiagram participant User participant App as Your Application participant Agent as Char Agent participant Hub as Tool Hub
(Durable Object) participant Tool as Tool
(Browser or MCP) User->>App: Uses application App->>Agent: Passes JWT token Agent->>Hub: Request (with token) Note over Hub: 1. Validate token signature Note over Hub: 2. Check issuer and audience Note over Hub: 3. Verify token not expired Note over Hub: 4. Extract user identity Note over Hub: 5. Load user's tool registry Hub->>Tool: Invoke tool Tool-->>Hub: Result Hub-->>Agent: Result Agent-->>User: Response ``` The Hub mediates every interaction. This centralization enables: * **Consistent authentication.** Every request validates the JWT—there's no path that bypasses verification. * **User-scoped tools.** The Hub loads only tools registered in the user's connections. No global tool registry exists. * **Origin validation.** The agent only operates on domains you've explicitly allowed. A valid token from an unauthorized origin is rejected. ## What This Enables The security architecture enables capabilities that would otherwise be too risky: **Cross-application tool invocation.** A user can invoke tools across multiple internal applications because identity flows through the Hub with proper scoping. The tool in Application A can call the tool in Application B—but only for that authenticated user. **External client access.** Claude Desktop or VS Code can invoke browser-based tools because authentication and authorization happen at the Hub, not the browser. The external client connects to your user's Hub, which routes requests to their browser tabs. **Stateful conversations.** Thread history persists across sessions because the Durable Object maintains state. Users can continue conversations—but only their own. ## The Tradeoffs This architecture makes specific tradeoffs: **IDP dependency.** Your identity provider's availability is Char's availability. There's no fallback, no local accounts, no emergency access. **No ambient context.** The agent can't "see" what the user sees. Page content must be explicitly exposed through tools. This limits what the agent can do without your developer effort. **Per-user state.** There's no way to share state between users. Cross-user workflows require explicit coordination. **Latency.** Token validation and Hub routing add latency compared to direct tool invocation. For real-time use cases, this matters. These tradeoffs are intentional. The alternatives—storing credentials, sharing state, bypassing validation—would reintroduce the risks the architecture is designed to prevent. ## Enterprise Security Features Additional security controls are available on the Enterprise plan: * **Rate limiting** - Per-user and per-endpoint throttling with configurable thresholds * **Audit logging** - Centralized logging of all tool invocations for compliance review * **Approval workflows** - Human-in-the-loop for sensitive operations * **Content guardrails** - PII detection and prompt injection filtering * **Kill switches** - Instant disable of tools or connectors The Hub-mediated architecture makes these controls straightforward to enable—they plug into the existing request flow without changing tool implementations. **Need these features?** [Contact us](mailto:alex@mcp-b.ai) to discuss Enterprise pricing. ## Further Reading How agents are isolated from ambient browser state How internal and external tools are distinguished Why Char delegates to your identity provider Technical specifications and configuration options # Session Inheritance Source: https://docs.usechar.com/explanation/session-inheritance AI agents inherit existing browser sessions with cookies and permissions—no OAuth flows or service accounts required When a user authenticates to your application, Char's embedded agent automatically inherits that session. No additional OAuth flows, no service accounts, no credential management. ## The Traditional Problem Integrating AI agents with enterprise applications typically requires: 1. **Service accounts** — Privileged accounts that act on behalf of users 2. **OAuth flows** — Users re-authenticate to grant the agent access 3. **Token management** — Refresh tokens, expiry handling, secure storage 4. **Elevated permissions** — Service accounts often have broader access than any individual user This creates both security risks and user friction. ## Char's Approach The agent inherits the user's existing session: ```mermaid theme={null} sequenceDiagram participant Agent as AI Agent (Client) participant Server as Backend Server (API) Note over Agent: Agent holds Inherited
Session Cookies Agent->>Server: API Request (e.g., GET /data) with Cookie: session_id=... Server->>Server: Verify Session Cookie (Bypass Auth) Server-->>Agent: API Response (200 OK, JSON data) Agent->>Server: API Request (e.g., POST /action) with Cookie: session_id=... Server->>Server: Verify Session Cookie Server-->>Agent: API Response (200 OK, Success) ``` The key insight: **WebMCP tools execute in the browser tab's context**. When a tool calls `fetch()`, it uses the user's existing cookies and session. ## Security Properties ### Permission Parity The agent has exactly the permissions the user has: | User Permission | Agent Permission | | ----------------------- | ----------------------- | | Can view records | Can view records | | Cannot delete records | Cannot delete records | | Read-only on financials | Read-only on financials | There's no privilege escalation path. The agent cannot access more than the user. ### Session Lifecycle When the user's session ends, the agent's access ends: * **Logout** — Agent loses access immediately * **Session timeout** — Agent cannot make calls * **Permission change** — Agent sees new permissions immediately No stale tokens. No cached credentials. The agent's access is always current. ### Audit Trail All agent actions appear as the user in your existing audit logs: ``` [2024-01-15 10:32:15] user@company.com updated customer #1234 [2024-01-15 10:32:18] user@company.com sent email to contact ``` Your existing compliance tooling works unchanged. ## Why This Works ### Browser as Authentication Layer The key insight is that WebMCP tools execute in the browser tab's JavaScript context—the same context where the user is already authenticated. When a tool makes a `fetch()` call, the browser automatically attaches session cookies. No code is needed to "pass" credentials; the browser handles it. This is fundamentally different from server-side agents, which must obtain and store credentials separately from the user's browser session. ### Standard Web Security Model Session inheritance doesn't bypass or modify web security—it relies on it: * **Same-origin policy** — Tools can only call APIs the page can reach * **Cookie scoping** — SameSite and HttpOnly attributes work as designed * **CORS enforcement** — Cross-origin restrictions apply normally The agent operates within the exact same security sandbox as any JavaScript on your page. Your existing security controls and audit mechanisms apply unchanged. For implementation details, see the [WebMCP Tools guide](/guides/webmcp-tools). ## Comparison: Session Inheritance vs Service Accounts | Aspect | Service Accounts | Session Inheritance | | ------------------ | --------------------- | ---------------------- | | Permissions | Often over-privileged | Exact user permissions | | Credential storage | Required | None | | Token management | Required | None | | Audit attribution | Service account | Actual user | | Session lifecycle | Independent | Tied to user | | Setup complexity | High | Zero | ## Design Assumptions Session inheritance assumes your application uses standard web authentication patterns: **Browser-mediated sessions** — Authentication state must be accessible to JavaScript running in the page. This typically means session cookies, though any browser-based auth (localStorage tokens, etc.) works. Server-side-only sessions without browser state cannot be inherited. **API accessibility** — Your APIs must be callable from the browser. If your backend only accepts server-to-server calls, tools cannot invoke it directly. Most modern web applications already expose browser-callable APIs for their frontend. ## Further Reading How tools are aggregated across apps JWT validation and identity verification Register tools in your application What the agent can and cannot see # Organization Skills Source: https://docs.usechar.com/explanation/skills How SKILL.md files teach your AI agent to orchestrate multi-step workflows using progressive disclosure and the AgentSkills specification Organization Skills are procedural knowledge for your agent. Think of the agent as your apprentice—skills are the documentation that teaches it how to accomplish tasks that require chaining multiple steps together. ## The Problem Skills Solve Your agent has access to tools: it can look up orders, send emails, process refunds. But knowing *how* to use each tool individually isn't the same as knowing *when* and *how* to orchestrate them together. Consider a return request. The agent needs to: verify the order exists, check eligibility against your policy, generate a return label, process the refund, and send confirmation. Without guidance, it might skip steps, do them out of order, or miss edge cases. Skills solve this by encoding your workflows. When a customer asks about returns, the agent loads the relevant skill and follows the documented procedure. It knows which tools to call, in what sequence, with what conditions. ## Why SKILL.md Format Char uses the [AgentSkills specification](https://agentskills.io), an open format originally developed by Anthropic and now adopted across the AI ecosystem. This matters for several reasons. First, portability. A skill you write for Char works in Claude Code, Cursor, and other compatible tools. You're not locked into a proprietary format. Second, simplicity. Skills are Markdown files with YAML frontmatter—the same format developers already use for documentation, blog posts, and configuration. There's no special syntax to learn, no compilation step, no deployment process. Third, version control. Skills are text files. You can store them in git, review changes in pull requests, and track who modified what and when. This is particularly valuable for regulated industries where audit trails matter. ## Progressive Disclosure Progressive Disclosure Diagram A naive approach would load all skills into every conversation. This wastes tokens and confuses the agent with irrelevant context. Char uses progressive disclosure instead, loading information only when the agent needs it. At startup, the agent receives a compact index: just the name and description of each skill. This typically runs 50-100 tokens per skill—enough for the agent to know what's available without consuming significant context. When a user's question matches a skill's domain, the agent requests the full content. The detailed instructions—which might run thousands of tokens—load only then. This keeps routine conversations fast and cheap while ensuring complex questions get the depth they need. The model is similar to how a human expert works. You don't rehearse every procedure you know before each conversation. You have a mental index of your expertise and dive deep when a question requires it. ## The Agent's Skill Tools The agent doesn't just consume skills passively—it has built-in tools to manage them: * **`read_skill`** loads full instructions when a task matches a skill's description * **`create_skill`** creates new skills from SKILL.md content * **`update_skill_with_patch`** makes targeted edits without rewriting the entire skill This means the agent can learn from conversations. When you describe a workflow, the agent can capture it as a skill for future use. When you refine a procedure, the agent can update the skill directly. The knowledge base grows through natural interaction. ## Skills vs. System Prompts You might wonder why skills exist when you could just put everything in the system prompt. The distinction becomes clear at scale. A system prompt is a single block of instructions that loads for every conversation. It's appropriate for universal guidance: your brand voice, safety rules, things the agent should always know. Skills are modular. They load selectively based on context. As your knowledge base grows—ten skills, fifty skills, hundreds of skills—this modularity becomes essential. You couldn't fit everything in a system prompt even if you wanted to. There's also a maintenance story. Updating a single skill doesn't require touching your core configuration. Teams can own their own skills. Changes are isolated and reviewable. ## The Broader Context Skills reflect a shift in how we think about AI customization. Traditional approaches required training data, compute resources, and machine learning expertise. You'd collect examples, fine-tune a model, deploy it, and hope it generalized well. Instruction-based customization is different. You tell the agent what to do in natural language. The feedback loop is immediate—change the instructions, see the behavior change. Domain experts who understand the business can contribute directly, without going through a technical translation layer. This doesn't replace fine-tuning entirely. If you need the model to recognize patterns it wasn't trained on or generate outputs in a specific style, training still has a role. But for teaching procedures, policies, and domain knowledge, instructions are often simpler and more transparent. ## Authorship and Collaboration Skills can come from multiple sources: dashboard users writing them manually, end users creating them through conversation, or the agent drafting them based on workflows you describe. This distributed model means your knowledge base grows organically. A support agent notices they keep explaining the same procedure and asks the agent to capture it. A power user documents a workflow for their colleagues. The barrier to contribution is low because you're just describing what should happen. All skills remain under organizational control. You can review, edit, and archive skills from the dashboard regardless of who created them or how they were created. ## Further Reading How skills become institutional memory Step-by-step guide Format specification Open standard # The Tool Hub Source: https://docs.usechar.com/explanation/tool-hub Why Char uses a per-user aggregation layer and how it enables cross-application workflows The Tool Hub is the central orchestration layer of Char—a per-user registry that aggregates tools from browser tabs, internal services, and external applications. Understanding why it exists and how it's built explains much of what makes Char's architecture distinctive. ## The Problem It Solves Traditional application integrations are point-to-point. If you want your CRM to access data from your ERP, you build an integration between them. Add a ticketing system? Build two more integrations. The work scales quadratically—N applications require up to N² integrations. ```mermaid theme={null} graph LR subgraph Point-to-Point A1[App A] <--> B1[App B] A1 <--> C1[App C] B1 <--> C1 A1 <--> D1[App D] B1 <--> D1 C1 <--> D1 end ``` AI agents promised to change this. Instead of rigid integrations, the agent would understand both systems and coordinate between them. But for this to work, the agent needs visibility into all the applications simultaneously. The naive solution—putting all tool definitions into a single system prompt—doesn't scale. Tools from five applications might fit. Tools from fifty won't. And it doesn't solve the execution problem: how does an agent invoke a tool that executes in a different context than where the agent runs? The Tool Hub solves both problems. It's a registry that tools register with dynamically, and a router that forwards invocations to wherever the tool actually executes—whether that's a browser tab, an internal service, or an external API. ## What Gets Aggregated The Hub aggregates tools from three distinct sources, each with different execution models: **Browser-based tools (WebMCP)** register when a page with Char embedded loads. The page announces its available tools over a WebSocket connection. When invoked, the Hub routes the request back to that specific browser tab, where execution happens using the user's session. **Internal remote MCP servers** register through connector configuration. These are services your enterprise operates—data platforms, internal APIs, custom tooling. They connect to the Hub over HTTP, and invocations route directly to them. Authentication uses the same identity that's already established for the user. **External MCP servers** also register through connector configuration, but with a different trust model. These are third-party services the user has authorized. The Hub manages OAuth tokens and routes invocations to the external service. ```mermaid theme={null} graph TD subgraph Sources["Tool Sources"] Browser[Browser Tabs
WebMCP Tools] Internal[Internal Services
Remote MCP] External[External Services
OAuth-Connected] end Hub[Tool Hub
Per-User Registry] Browser <-->|WebSocket| Hub Internal <-->|HTTP| Hub External <-->|HTTP + OAuth| Hub Agent[AI Agent] <-->|Unified Interface| Hub ``` From the agent's perspective, these distinctions are invisible. It sees a unified list of tools and invokes them the same way. The Hub handles the routing complexity. ## Why Per-User Scoping A simpler architecture would have a single Tool Hub per organization. All users share the same registry, tools are available to everyone. This creates problems: **Permission confusion.** Users have different permissions in different applications. Alice might have admin access to the CRM but read-only access to the ERP. A shared registry can't model this—either everyone sees all tools (security risk) or tools need complex permission logic (complexity explosion). **Session isolation.** Browser-based tools execute using browser sessions. Alice's CRM session is different from Bob's CRM session. If they shared a registry, whose session would the tool use? **Connector scoping.** Remote MCP connections carry user identity. A shared Hub would need to multiplex connections per-user anyway, defeating the simplification. **Privacy.** Users might not want colleagues seeing what applications they have open, what external services they've connected, or what tools they're using. Per-user scoping sidesteps all of this. Alice has her own Hub with her tools, using her sessions and her connections. Bob has his own Hub. There's no complexity from trying to model multi-user access because there's no multi-user access to model. The cost is that users can't directly share tools or collaborate through the Hub. In practice, this isn't a limitation—users collaborate through the applications themselves, not through the agent infrastructure. ## Why Cloudflare Durable Objects The Tool Hub is implemented as a Cloudflare Durable Object. This choice deserves explanation because it has significant implications. Durable Objects provide **strongly consistent, single-threaded state** at the edge. Each Hub instance is a JavaScript object with its own storage, guaranteed to run in a single location at any given time. This means: **No coordination overhead.** When multiple browser tabs and remote services connect to the same Hub, they connect to the same physical instance. There's no distributed consensus, no eventual consistency, no race conditions between replicas. The Hub's state is always coherent. **Geographic affinity.** Durable Objects run close to where they're accessed. If Alice is in London, her Hub runs in a European datacenter. WebSocket connections from her browser tabs and HTTP connections from internal services have low latency. **Automatic scaling.** Each user's Hub is a separate instance. A million users means a million independent Hubs, each handling its own load. There's no shared infrastructure to become a bottleneck. **Infrastructure-level isolation.** Cross-user access isn't just prohibited by application logic—it's structurally impossible. There's no code path that could retrieve another user's Hub because each Hub is a distinct entity in Cloudflare's infrastructure. ```mermaid theme={null} graph TD subgraph Alice["Alice's Context"] A1[Browser Tab 1] A2[Browser Tab 2] A3[Internal MCP] end subgraph Bob["Bob's Context"] B1[Browser Tab 1] B2[Internal MCP] end AH[Alice's Durable Object
London datacenter] BH[Bob's Durable Object
NYC datacenter] A1 <-->|WebSocket| AH A2 <-->|WebSocket| AH A3 <-->|HTTP| AH B1 <-->|WebSocket| BH B2 <-->|HTTP| BH ``` ### Alternatives Considered **Redis or traditional databases.** These require careful sharding to avoid hotspots, complex coordination for WebSocket routing, and explicit isolation logic. They work, but they trade operational simplicity for flexibility we don't need. **In-memory state with sticky sessions.** This loses durability—if the server restarts, state is lost. It also complicates deployment across multiple regions. **Actor frameworks (Akka, Orleans).** These could work but require managing infrastructure. Durable Objects provide the actor model as a managed service. The Durable Objects choice reflects a broader principle: use infrastructure-level guarantees where possible rather than application-level complexity. ## How Tool Registration Works Tools register differently depending on their source. ### Browser-Based Tools When a browser tab with Char embedded loads, it establishes a WebSocket connection to the user's Tool Hub. Through this connection, the page registers its available tools: ```mermaid theme={null} sequenceDiagram participant Page as Browser Page participant Hub as Tool Hub Page->>Hub: Connect (WebSocket) Hub-->>Page: Connected Page->>Hub: Register tools [search, updateRecord, ...] Hub->>Hub: Add to registry with tab namespace Hub-->>Page: Registered Note over Hub: Registry now contains
crm_search, crm_updateRecord, ... ``` When the browser tab closes, its WebSocket disconnects, and the Hub removes those tools from the registry. The Hub reflects reality—you can only invoke tools from applications that are actually open. ### Remote MCP Servers Internal and external MCP servers register through connector configuration at the organization level. The Hub discovers their tools by calling the standard MCP `tools/list` endpoint: ```mermaid theme={null} sequenceDiagram participant Hub as Tool Hub participant MCP as Remote MCP Server Hub->>MCP: tools/list (with user identity) MCP-->>Hub: Available tools [queryData, createTicket, ...] Hub->>Hub: Add to registry with connector namespace Note over Hub: Registry now contains
dataplatform_queryData, ticketing_createTicket, ... ``` The Hub caches tool schemas but refreshes them periodically and on-demand. If a remote server adds or removes tools, the Hub discovers this on the next sync. ## Tool Invocation Routing When a tool is invoked, the Hub routes the request to the correct destination: ```mermaid theme={null} sequenceDiagram participant Agent as AI Agent participant Hub as Tool Hub participant Tab as Browser Tab participant MCP as Remote MCP Agent->>Hub: Call crm_updateRecord({...}) Hub->>Hub: Lookup tool source alt Browser-based tool Hub->>Tab: Forward request (WebSocket) Tab->>Tab: Execute with user session Tab-->>Hub: Result else Remote MCP tool Hub->>MCP: tools/call (HTTP) MCP-->>Hub: Result end Hub-->>Agent: Return result ``` For browser-based tools, the Hub never sees the actual API calls or data—it only routes messages. Execution happens entirely in the browser tab. For remote MCP tools, the Hub acts as an authenticated proxy. It attaches appropriate credentials and forwards the request. The remote server sees a request from the authenticated user, not from a generic service account. ## What the Hub Enables This architecture enables capabilities that would otherwise require significant integration work: **Cross-source workflows.** An agent can invoke browser-based CRM tools, internal data platform queries, and external calendar operations in a single workflow. The Hub unifies them under one interface. **External client access.** Claude Desktop or VS Code can invoke all tools via the MCP protocol—browser-based tools, internal services, external applications. The Hub bridges desktop applications and the full tool ecosystem. **Centralized policy.** Because all tool invocations flow through the Hub, policy enforcement happens in one place. Classification, approval workflows, and audit logging don't need to be implemented in each application or service. **Graceful degradation.** If a browser tab closes, its tools become unavailable. If a remote service goes down, those tools stop working. The Hub tracks what's actually available, not what might theoretically be available. **Identity continuity.** Every tool invocation carries the same user identity. Browser tools use the user's session. Remote tools use tokens derived from the user's IDP token. The audit trail is consistent across all sources. ## The Tradeoffs The Hub architecture involves real tradeoffs: **Browser dependency for WebMCP.** Browser-based tools require browser tabs. No tabs, no tools. This is intentional—browser-based execution enables session inheritance—but it means WebMCP isn't suitable for background automation in its basic form. For unattended workflows, enterprise deployments can use remote browsers with delegated credentials. **Latency.** Cross-application tool calls involve multiple hops. Browser tools: agent → Hub → browser tab → application API → browser tab → Hub → agent. Remote tools: agent → Hub → remote server → Hub → agent. For interactive use cases, the latency is acceptable. For high-frequency automation, it might not be. **Complexity for simple cases.** If you only have one application and don't need cross-app workflows, the Hub adds architectural complexity without benefit. Char's Tier 0 (page-scoped) mode exists for these cases. **Connector management.** Remote MCP connections require configuration—server URLs, authentication settings, trust classification. This is operational overhead that browser-based tools don't have. ## Further Reading The problem Char solves and the three tool sources How Hub-mediated execution enables context capture Why browser-based tools work the way they do When you need the Hub vs simpler modes # Trust Boundaries Source: https://docs.usechar.com/explanation/trust-boundaries How Char distinguishes between internal and external tools, and why this matters for security Every security model must decide what to trust. Char's trust model is built around a simple principle: your enterprise is responsible for the security of its own systems. Char trusts what you trust, and applies scrutiny to everything else. ## The Enterprise as Trust Root When Char validates a user's JWT token, it establishes trust through your identity provider. The user is who your IDP says they are. Their permissions are what your IDP grants them. This same principle extends to tools. MCP servers that authenticate through your enterprise IDP are part of your enterprise. They're internal. Everything else is external. The distinction matters because it determines what security controls apply: **Internal tools** run without content inspection. Your enterprise wrote the code, controls the deployment, and takes responsibility for its behavior. Char doesn't second-guess your internal systems any more than your internal APIs second-guess your database. **External tools** are treated as untrusted. The assumption is that external content is potentially hostile. On the Enterprise plan, external tool responses pass through content inspection before reaching the agent. ## Why Internal Tools Are Trusted This trust model sometimes surprises security reviewers. Shouldn't all content be validated? Isn't defense-in-depth about multiple layers everywhere? The answer is practical. Consider what it would mean to distrust internal tools: **False positives.** Internal tools often return data that looks suspicious—code snippets, configuration files, documentation with examples. Content validation aggressive enough to catch real attacks would block legitimate internal workflows. **Performance cost.** Content inspection adds latency. Running it on every internal tool response would significantly slow down the agent. **Operational burden.** Every false positive needs investigation. Security teams would spend their time reviewing legitimate internal traffic instead of actual threats. **Implicit insult.** Distrusting internal systems implies the enterprise can't secure its own infrastructure. This isn't a position Char can credibly take—if internal systems are compromised, the attacker has access to far more than the agent. The alternative—treating internal and external tools identically—would either overwhelm users with false positives or weaken protection against actual external threats. ## What Makes a Tool Internal A tool is internal if it authenticates to Char using your enterprise's identity infrastructure. This typically means one of: **Same IDP.** The MCP server validates tokens against the same identity provider as your embedded agent. When a tool call arrives, the server trusts it because it comes from a user your IDP authenticated. **VPC connectivity.** The MCP server runs in your private network and accepts connections only from your Char deployment through a VPC tunnel. Network isolation provides the trust boundary. **Service-to-service authentication.** The MCP server accepts requests signed with credentials your enterprise manages. The credential's validity establishes trust. In all cases, the enterprise controls both ends of the connection. The tool is internal because your enterprise vouches for it. ## What Makes a Tool External External tools come from third parties. They might be SaaS applications, public APIs, or MCP servers operated by vendors. The defining characteristic is that your enterprise doesn't control the code or the deployment. External tools authenticate differently. Instead of your IDP validating the user, the tool validates its own users—or accepts tokens that your users consented to share. The trust relationship is mediated by the user's consent, not the enterprise's control. This mediation creates risk. The external tool might: * Return content designed to manipulate the agent * Leak information it shouldn't have access to * Behave differently than its documentation claims The trust boundary provides protection: external tools are identified and tracked separately from internal tools, enabling content inspection, rate limiting, and audit logging on the Enterprise plan. ## Enterprise-Approved External Tools Some external tools occupy a middle ground. The enterprise has reviewed them, approved their use, and configured them in the connector registry—but they're still operated by third parties. These enterprise-approved external tools are tracked differently than fully untrusted tools. The rationale: if the enterprise approved the tool, they've accepted its intended functionality. The approval itself is the security control. ## User-Consented External Tools The riskiest category is tools that users connect without enterprise approval. A user might authorize a third-party MCP server that provides convenient functionality but hasn't been reviewed for security. Char tracks user-consented tools separately from enterprise-approved tools. This classification enables: * Different audit treatment (Enterprise plan) * Different rate limits (Enterprise plan) * Organization-level policies to disable user-consented tools entirely Content inspection for external tools (PII detection, prompt injection filtering) is available on the Enterprise plan. ## The Responsibility Boundary The trust model creates a clear responsibility boundary: **Char is responsible for:** * Correctly identifying whether a tool is internal or external * Enforcing infrastructure-level isolation between users * Validating authentication tokens on every request * Maintaining the trust boundary classification **The enterprise is responsible for:** * The security of internal MCP servers * Reviewing and approving external tool connections * Monitoring their own systems for anomalies * Responding to security events in their infrastructure This division matches how enterprises already think about security. Your SOC monitors your internal systems. Your vendor management process reviews third-party tools. Char fits into these existing workflows rather than replacing them. ## Further Reading The overall security design and its rationale How agents are isolated from ambient browser state Technical specifications for security controls Set up internal and external MCP connections # Configure Allowed Domains Source: https://docs.usechar.com/guides/allowed-domains Specify which origins can embed the Char agent The Char agent only operates on origins you explicitly allow. This guide shows you how to configure allowed domains for your organization. ## Add an Allowed Domain 1. Open the [Char dashboard](https://app.usechar.ai) 2. Navigate to **Settings** → **Integration** 3. Under **Allowed Domains**, click **Add Domain** 4. Enter the full origin (e.g., `https://app.example.com`) 5. Click **Save** The domain is active immediately. No deployment or restart required. ## Origin Format Origins must be complete—scheme, hostname, and port (if non-standard): ``` https://app.example.com https://staging.example.com:3000 http://localhost:3000 ``` Omit trailing slashes. Wildcards are not supported. ## Common Configurations **Production + staging:** ``` https://app.example.com https://staging.example.com ``` **Multiple subdomains:** ``` https://app.example.com https://admin.example.com https://support.example.com ``` **Local development:** `localhost` and `127.0.0.1` are allowed by default on any port. You don't need to add them. ## Remove a Domain 1. Open **Settings** → **Integration** 2. Find the domain in the **Allowed Domains** list 3. Click the delete icon 4. Confirm removal Removal is immediate. Agents on that origin will stop authenticating. ## Troubleshooting **Agent shows authentication error:** Check that the origin in your browser's address bar exactly matches an allowed domain. Common mismatches: * Missing `https://` prefix * Including a trailing slash * Wrong port number * Subdomain not listed **Agent works locally but not in production:** Verify you've added the production origin. `localhost` is allowed automatically, but `https://app.example.com` must be added explicitly. **CORS errors in console:** The allowed domains list only controls Char authentication, not CORS. If you see CORS errors, check that your server's CORS configuration permits requests from the Char API. # Identity Modes Source: https://docs.usechar.com/guides/autonomous-char/identity User-scoped, agent identity, or hybrid **Private Beta** — [Contact us](mailto:alex@mcp-b.ai?subject=Autonomous%20Char) to get set up. Autonomous execution raises a question: *whose identity does the agent use?* The identity determines what the agent can access—both browser-based applications (via token injection) and remote MCP servers (via [ID-JAG](/reference/remote-mcp-auth) token exchange). Three models are supported. ## User-scoped execution The agent acts as a specific user. Their identity, their permissions, their audit trail—just without them present. ```mermaid theme={null} sequenceDiagram participant Agent as Autonomous Char participant IDP as Your IDP participant Browser as Browser Tabs participant MCP as Remote MCP Servers Agent->>IDP: Token exchange (as user) IDP-->>Agent: User-scoped token Agent->>Browser: Inject credentials Agent->>MCP: ID-JAG (user identity) Note over Browser,MCP: Acts as the user everywhere ``` This is [session inheritance](/explanation/session-inheritance) extended to unattended execution. The agent can only do what the user could do—in browser applications and in remote MCP servers. Audit logs show the user as the actor. **When to use it:** * Tasks the user would do themselves, just automated * Accessing resources the user already has permission for * When accountability should trace back to a specific person **The tradeoff:** The user's permissions must cover everything the agent needs—both browser access and MCP server access. If the agent needs access the user doesn't have, you either grant the user broader permissions (security risk) or the task fails. ## Agent identity The agent has its own identity in your IDP—a service principal, workload identity, or machine account registered specifically for this agent. ```mermaid theme={null} sequenceDiagram participant Agent as Autonomous Char participant IDP as Your IDP participant Browser as Browser Tabs participant MCP as Remote MCP Servers Agent->>IDP: Authenticate as agent IDP-->>Agent: Agent credentials Agent->>Browser: Inject credentials Agent->>MCP: ID-JAG (agent identity) Note over Browser,MCP: Acts as the agent everywhere ``` The agent authenticates as itself, not as a user. It has its own permissions for browser applications, its own role assignments for MCP servers, its own audit trail. When it acts, the logs show the agent as the actor. **When to use it:** * Agents that need permissions no single user should have * Cross-functional workflows spanning multiple users' domains * When the agent's decisions are its own, not a user's automation * Compliance scenarios requiring clear human/agent separation **The tradeoff:** You need to provision and manage agent identities in your IDP. The agent needs its own role assignments for both browser apps and MCP servers. This is more infrastructure, but cleaner accountability. ## Hybrid: Agent acting on behalf of user There's a middle ground. The agent has its own identity, but the token also preserves who authorized it. This uses the `act` (actor) claim pattern from [RFC 8693 Token Exchange](https://datatracker.ietf.org/doc/html/rfc8693): ```json theme={null} { "sub": "user-123", // Who the data belongs to "act": { "sub": "agent-456" // Who decided to act } } ``` The `sub` is the user—their permissions apply (for both browser apps and MCP servers). The `act` is the agent—visible in audit logs. This separates "whose authority is being used" from "who made the decision to use it." ```mermaid theme={null} sequenceDiagram participant Agent as Autonomous Char participant IDP as Your IDP participant Browser as Browser Tabs participant MCP as Remote MCP Servers Agent->>IDP: Token exchange (OBO) IDP-->>Agent: Token with act claim Agent->>Browser: Inject credentials Agent->>MCP: ID-JAG (user + agent) Note over Browser,MCP: User's access, agent's attribution ``` **When to use it:** * When you need user-scoped permissions but agent-attributed actions * Compliance scenarios that require distinguishing human actions from agent actions * Multi-agent workflows where you need to track which agent did what This is what Microsoft Entra's [Agent ID OBO flow](https://learn.microsoft.com/en-us/entra/agent-id/identity-platform/agent-identities) implements, and what standards-based token exchange enables. ## Choosing between them | Consideration | User-scoped | Hybrid (OBO) | Agent identity | | ------------------ | ----------------- | ----------------------------------- | -------------------- | | **Permissions** | User's access | User's access | Agent's own roles | | **Audit trail** | "User did X" | "Agent did X (as user)" | "Agent did X" | | **Accountability** | User | User authorized, agent acted | Agent (and sponsor) | | **IDP setup** | Token exchange | Token exchange + agent registration | Agent registration | | **Best for** | Simple automation | Attributed automation | Autonomous decisions | Many organizations start with user-scoped execution for simple automation, move to hybrid OBO when they need attribution, then to full agent identities as agents become truly autonomous. ## Further reading * [Explaining OAuth Delegation, OBO, and Agent Identity](https://blog.christianposta.com/explaining-oauth-delegation-on-behalf-of-and-agent-identity-for-ai-agents/) — Christian Posta on how token exchange works for agents * [Agent Identity: Impersonation or Delegation?](https://blog.christianposta.com/agent-identity-impersonation-or-delegation/) — The accountability tradeoffs * [What Kind of Identity Should Your AI Agent Have?](https://aembit.io/blog/what-kind-of-identity-should-your-ai-agent-have/) — Aembit's analysis of identity options * [Agent Identities in Microsoft Entra](https://learn.microsoft.com/en-us/entra/agent-id/identity-platform/agent-identities) — Microsoft's approach to agent identity * [Identity 101 for AI Agents](https://www.secureauth.com/agentic-ai/white-paper-identity-101-for-ai-agents/) — SecureAuth whitepaper on the fundamentals # Autonomous Char Source: https://docs.usechar.com/guides/autonomous-char/index Unattended execution in cloud browsers **Private Beta** — [Contact us](mailto:alex@mcp-b.ai?subject=Autonomous%20Char) to get set up. Autonomous Char runs unattended in a cloud browser—without user presence or a local machine. It connects to the [Tool Hub](/explanation/tool-hub) and has access to all tool sources: internal MCP servers, external MCP servers, and browser-based WebMCP tools. ## The problem Some workflows should run without a human watching. Scheduled tasks. Background processing. Batch operations. You want an AI agent to do the work, but you don't want to keep a browser open on someone's laptop. ## How it works Autonomous Char combines two things: **The [Char Extension](/guides/char-extension)** runs in a cloud browser service (like Browserbase). It uses Chrome's tabs APIs to navigate pages, inject [user scripts](/guides/extension-user-scripts), and execute WebMCP tools against legacy portals and SaaS applications. **The [Tool Hub](/explanation/tool-hub) connection** gives access to everything else—[internal MCP servers](/guides/internal-mcp-servers), [external MCP servers](/guides/external-mcp-servers), and any other connectors configured for your org. ```mermaid theme={null} graph LR subgraph Cloud["Cloud Browser"] Ext[Char Extension] Tab[Browser Tabs] end Hub[Tool Hub] subgraph Remote["Remote MCP Servers"] Internal[Internal MCP] External[External MCP] end Ext <-->|WebSocket| Hub Ext -->|WebMCP| Tab Hub <--> Internal Hub <--> External ``` The extension handles browser automation via WebMCP tools. The Hub routes calls to remote MCP servers—internal APIs and external services. Together, they give the agent access to the full tool surface. ## What the extension provides The Char Extension running in the cloud browser provides: * **Tab management** via Chrome's tabs APIs * **Domain allowlisting** controls which sites the agent can visit * **User script injection** for WebMCP tools on legacy applications * **Token injection** for authenticating to applications (see [Identity Modes](/guides/autonomous-char/identity)) ## What the Tool Hub provides Through the Hub connection, Autonomous Char also has access to: * **[Internal MCP servers](/guides/internal-mcp-servers)** — your data platforms, internal APIs, custom tooling * **[External MCP servers](/guides/external-mcp-servers)** — third-party services connected via OAuth * **Cross-app tools** — any connector registered for your org The agent can combine browser automation with backend API calls in a single workflow. ## Boundaries Autonomous Char doesn't roam freely. **Browser boundaries:** Domain allowlists control which sites the extension can visit. It can only navigate to configured domains. **Tool boundaries:** The same [governance](/guides/governance/index) controls apply to all tool calls: * **Connector registry** determines available tools * **Role entitlements** scope access * **Guardrails** inspect content * **Approvals** gate sensitive operations * **Audit** logs everything ## What you get **Unattended execution.** Workflows run without human presence. Scheduled, triggered, or on-demand. **Full tool access.** Browser-based WebMCP tools plus remote MCP servers. The complete tool surface, not just browser automation. **Flexible identity.** Choose user-scoped for simple automation, hybrid for attributed actions, or agent identity for autonomous systems. **Full governance.** Every tool call—browser and backend—flows through the same controls as interactive sessions. ## What's in this section User-scoped, agent identity, or hybrid ## See also Browser automation component Connect to internal APIs How tools are aggregated # Char Extension Source: https://docs.usechar.com/guides/char-extension Add tools to applications you don't control **Private Beta** — [Contact us](mailto:alex@mcp-b.ai?subject=Char%20Extension) to get set up. The Char Extension is a Chrome extension that connects browsers to the [Tool Hub](/explanation/tool-hub). It adds tools to legacy portals, third-party SaaS, and any application where you can't embed Char directly. ## The problem Not every application will add a script tag for you. Legacy portals maintained by another team. Third-party SaaS you don't control. Internal tools built years ago that nobody wants to touch. You want AI agents to interact with these applications. But you can't modify them. ## How it works The extension's service worker connects to the Tool Hub via WebSocket—same protocol as the embedded agent, different runtime. ```mermaid theme={null} graph TD subgraph Browser SW[Extension] Tab1[Legacy Portal] Tab2[Third-Party SaaS] end Hub[Tool Hub] SW <-->|WebSocket| Hub SW -->|Inject scripts| Tab1 SW -->|Inject scripts| Tab2 Tab1 -->|registerTool| SW Tab2 -->|registerTool| SW ``` [User scripts](/guides/extension-user-scripts) inject into matching pages via Chrome's user scripts API. They call `registerTool()` to expose functionality. The extension aggregates tools from all tabs and uplinks them to the Hub. ## Where it runs | Environment | Use case | | ----------------- | ------------------------------------------------------------------------- | | **Local browser** | Add tools to apps you use daily | | **Cloud browser** | [Autonomous Char](/guides/autonomous-char/index) for unattended execution | The architecture is identical. Same extension, same Hub connection, different runtime. ## What you get **Tools where you couldn't have them.** Legacy applications become AI-accessible without modifying their source code. **Org-level scripts.** Scripts are defined at the org level and shared across users. Everyone gets the same tools for the same applications. **Same governance.** Tool calls from the extension flow through the same [governance](/guides/governance/index) layer as everything else—access control, guardrails, approvals, audit. ## See also Write scripts that inject tools Test extension tools from Claude Code Run unattended in cloud browsers # Managing Char from Claude Code Source: https://docs.usechar.com/guides/char-saas-mcp Use the Char SaaS MCP to configure your organization, set up identity providers, and access browser tools from Claude Code The Char SaaS MCP connects Claude Code to your Char account, letting you manage everything through conversation instead of switching between terminal and dashboard. ## Prerequisites * Claude Code installed ([download](https://claude.ai/download)) * The Char SaaS MCP server added to your configuration (see [Install MCP Servers](/guides/mcp-servers)) ## First-Time Setup When you first use the Char SaaS MCP, an authentication flow runs automatically: A browser window opens to the Char sign-in page. Sign in with your identity provider, or create a new account. If you're new, name your organization. Char suggests a name based on your email domain (e.g., "Acme" for `user@acme.com`). Confirm that Claude Code can access your Char account. Click **Authorize** and the browser closes. Back in Claude Code, you're authenticated. Your session persists across restarts. ## Common Tasks ### Configure an Identity Provider Ask Claude to set up your IDP: ``` Configure Okta as my identity provider. My issuer URL is https://acme.okta.com and my client ID is 0oa1234567890abcdef. ``` Claude uses the `update_organization` tool to save your configuration. ### Check Your Organization ``` Show me my Char organization details ``` Returns your org name, configured IDP, allowed domains, and subscription tier. ### Add Allowed Domains ``` Add app.acme.com and staging.acme.com to my allowed domains ``` These are the origins where you can embed the Char agent. ### Access Browser Tools If you have browser tabs open with Char embedded, you can invoke those tools from Claude Code: ``` List the tools available in my browser tabs ``` ``` Call the crm.searchContacts tool with query "Acme Corp" ``` This uses the [Tool Hub](/explanation/tool-hub) to route calls to your browser. ## Available Tools | Tool | Purpose | | --------------------- | --------------------------------------------- | | `get_organization` | View org details, IDP config, allowed domains | | `update_organization` | Configure IDP, update allowed domains | | `get_subscription` | Check plan tier and features | | `hub_list_tools` | List tools from connected browser tabs | | `hub_call_tool` | Invoke a browser tool | For the full API, see [MCP Tools Reference](/reference/mcp-tools). ## Session Persistence Your authentication persists across Claude Code sessions: * Restart Claude Code → still authenticated * Close terminal → still authenticated * Next day → still authenticated Re-authentication is only needed if you explicitly sign out or your session expires. ## Troubleshooting * Check that your default browser is set correctly * Try running Claude Code from a terminal with GUI access * On headless systems, copy the URL from the output and open it manually * Verify you're signing into the correct account * Check that third-party cookies aren't blocked * Try in an incognito window to rule out extensions * Ensure you have browser tabs open with Char embedded * Check that those pages have registered WebMCP tools * Verify you're authenticated to those applications ## See Also Add the Char SaaS MCP to Claude Code How browser tools connect to Claude Code Full API documentation Register tools in your application # Get Started with Claude Code Source: https://docs.usechar.com/guides/claude-code-setup Set up Char in your app using the Claude Code plugin and the publishable-key auth model Use the Char plugin to install and wire the agent quickly. Prefer manual setup? See [Get Started Manually](/quickstart). ## Prerequisites * Claude Code installed * A web app project * A Char account/org ## Step 1: Install plugin ```bash theme={null} /plugin marketplace add WebMCP-org/char-plugin /plugin install char@webmcp ``` ## Step 2: Run setup ```text theme={null} /char:setup ``` The setup flow installs the package, adds ``, and configures integration files. ## Step 3: Add your publishable key Use a key from **Dashboard → Settings → Publishable Keys**: ```html theme={null} ``` If your app has end-user auth, pass an ID token with `connect()`. ## Debug tools ```html theme={null} ``` ## Next steps # Configure CORS and CSP Source: https://docs.usechar.com/guides/cors-csp Set up Content Security Policy and CORS headers for the Char agent The Char agent connects to external services and loads resources. If your application uses Content Security Policy (CSP) headers, you'll need to allowlist Char's domains. ## Content Security Policy ### Minimum Required Policy Add these directives to your CSP header: ```http theme={null} Content-Security-Policy: default-src 'self'; connect-src 'self' https://*.usechar.ai wss://*.usechar.ai; script-src 'self' https://unpkg.com; style-src 'self' 'unsafe-inline'; ``` **`'self'` does not cover WebSocket connections.** You must explicitly list `wss://*.usechar.ai` in `connect-src`. This is a [common pitfall](https://github.com/w3c/webappsec-csp/issues/7) — the `'self'` keyword matches only the same scheme, host, and port. ### Directive Breakdown | Directive | Values | Purpose | | ------------- | ---------------------- | -------------------------------- | | `connect-src` | `https://*.usechar.ai` | HTTPS API requests | | `connect-src` | `wss://*.usechar.ai` | WebSocket connection to Tool Hub | | `script-src` | `https://unpkg.com` | Agent script from CDN | | `style-src` | `'unsafe-inline'` | Agent component styles | ### About `'unsafe-inline'` for Styles The Char agent uses web components with shadow DOM, which inject styles dynamically. This currently requires `'unsafe-inline'` in `style-src`. While `'unsafe-inline'` weakens CSP protection against style-based attacks, the primary security value of CSP comes from restricting `script-src`. Style injection attacks are significantly less severe than script injection. See [OWASP CSP Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Content_Security_Policy_Cheat_Sheet.html) for context. **Alternative with nonces** (if your framework supports it): ```http theme={null} Content-Security-Policy: style-src 'self' 'nonce-{random}'; ``` However, web components that create styles in shadow DOM may not respect nonces. If you need strict CSP without `'unsafe-inline'`, contact us about self-hosting options. ### Self-Hosted Agent If you bundle the agent with your application instead of loading from CDN: ```http theme={null} Content-Security-Policy: default-src 'self'; connect-src 'self' https://*.usechar.ai wss://*.usechar.ai; style-src 'self' 'unsafe-inline'; ``` This removes the need for `unpkg.com` in `script-src`. ### Strict CSP Starting Point For applications starting with a strict policy, here's a complete example: ```http theme={null} Content-Security-Policy: default-src 'none'; script-src 'self' https://unpkg.com; style-src 'self' 'unsafe-inline'; connect-src 'self' https://*.usechar.ai wss://*.usechar.ai; img-src 'self' data:; font-src 'self'; base-uri 'self'; form-action 'self'; ``` Starting with `default-src 'none'` and explicitly allowing only what's needed is the [recommended approach](https://content-security-policy.com/). ## CORS Configuration The Char API includes appropriate CORS headers for browser requests. Your server doesn't need to proxy Char API calls. ### Reverse Proxy Considerations If you're running a reverse proxy or API gateway, ensure it doesn't strip CORS headers from Char API responses: **Nginx:** ```nginx theme={null} location / { # Preserve CORS headers from upstream proxy_pass_header Access-Control-Allow-Origin; proxy_pass_header Access-Control-Allow-Headers; proxy_pass_header Access-Control-Allow-Methods; proxy_pass_header Access-Control-Allow-Credentials; } ``` **Apache:** ```apache theme={null} # Don't modify upstream CORS headers Header always unset Access-Control-Allow-Origin "expr=%{REQUEST_URI} =~ m#^/api/#" ``` **Cloudflare / Vercel / Netlify:** These platforms preserve CORS headers by default. No configuration needed. ### CORS vs CSP These are different mechanisms: | Mechanism | Controls | Direction | | --------- | --------------------------------- | ---------------- | | **CORS** | Which origins can read responses | Server → Browser | | **CSP** | Which resources the page can load | Page → Browser | CSP's `connect-src` does not bypass CORS. Both must allow the connection. ## Troubleshooting ### "Refused to connect to wss\://" **Cause:** WebSocket URL not in `connect-src`. **Fix:** Add `wss://*.usechar.ai` to `connect-src`. Note that `'self'` does not cover WebSocket schemes. ### "Refused to load the script" **Cause:** Script source not allowed by `script-src`. **Fix:** Add `https://unpkg.com` to `script-src`, or self-host the agent. ### "Refused to apply inline style" **Cause:** `style-src` doesn't allow inline styles. **Fix:** Add `'unsafe-inline'` to `style-src`. This is currently required for the agent's web components. ### Agent loads but API calls fail **Cause:** Reverse proxy stripping CORS headers. **Fix:** Configure your proxy to pass through `Access-Control-*` headers. Check the Network tab — responses should include `Access-Control-Allow-Origin`. ### Mixed content errors **Cause:** Page loaded over HTTPS but trying to connect via HTTP. **Fix:** Ensure all Char URLs use `https://` and `wss://` (not `http://` or `ws://`). ## Verifying Your Configuration Open browser developer tools and check: 1. **Console** — No CSP violation errors (red text mentioning "Content Security Policy") 2. **Network** → Filter by "WS" — WebSocket connection shows status 101 3. **Network** → Filter by domain — Requests to `usechar.ai` return 200 ## Report-Only Mode Test your CSP without breaking functionality: ```http theme={null} Content-Security-Policy-Report-Only: default-src 'self'; connect-src 'self' https://*.usechar.ai wss://*.usechar.ai; report-uri /csp-reports; ``` This logs violations without blocking them. Review reports, then switch to enforcing mode. ## References * [MDN: Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) * [MDN: connect-src directive](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/connect-src) * [OWASP: CSP Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Content_Security_Policy_Cheat_Sheet.html) * [CSP Quick Reference](https://content-security-policy.com/) # Creating Skills Source: https://docs.usechar.com/guides/creating-skills How to create skills that teach your agent to handle specific tasks This guide shows how to create skills using three methods: asking the agent, using the dashboard, or via API. New to skills? Read [Understanding Skills](/explanation/skills) first. ## Where Skills Live In this product, organization skills are stored in the backend database and scoped to your current organization. * Create and edit them from the **Skills** page. * The agent can also create and update them with `create_skill` and `update_skill_with_patch`. * Each skill is still authored as full `SKILL.md` content (YAML frontmatter + Markdown body). In addition to org-managed skills, the runtime includes one global built-in skill: * `skill-creator-webmcp` at location `builtin://skill-creator-webmcp` The built-in skill is shared across organizations and is read-only. ## Ask the Agent The agent has a built-in `create_skill` tool. Describe the workflow you want to capture: > "Create a skill for processing return requests. First verify the order exists, check if it's within 30 days and unused, generate a return label, then send confirmation." To refine an existing skill: > "Update the returns skill to require photo proof for items over \$100" The agent uses `update_skill_with_patch` to make targeted edits. ## From the Dashboard 1. Go to **Skills** in the sidebar 2. Click **Create Skill** 3. Enter a name (lowercase, hyphens only) 4. Write a description of when the skill should activate 5. Add workflow steps in the body 6. Click **Save** ## From the API See [Skill Schema — API](/reference/skill-schema#api) for endpoints and request formats. ## Writing the Description The description determines when the agent loads the skill. Be specific: | Vague | Specific | | ------------------ | ------------------------------------------------------------------------ | | `Handle orders` | `Check order status, provide tracking links, explain delivery estimates` | | `Customer support` | `Process returns, explain refund timelines, generate shipping labels` | ## Writing the Body Structure the body as numbered steps. Reference the tools the agent should use: ```markdown theme={null} --- name: process-return description: Process return requests by verifying eligibility and generating labels --- # Process Return ## 1. Verify Order - Use `orders.lookup` with order ID or customer email - Confirm the order belongs to this customer ## 2. Check Eligibility - Within 30 days of delivery - Unused, original packaging - Not a final-sale item ## 3. Generate Label - Call `shipping.create_return_label` - Save the tracking number ## 4. Confirm - Send email with label and refund timeline (5-7 days) ``` ## Testing Ask the agent a question that should trigger the skill. If it doesn't activate: * Check the description matches the question type * Verify the name format (lowercase, no consecutive hyphens) * Check for validation errors in the dashboard ## Next Steps Field definitions and validation rules How progressive disclosure works # Custom Styling Source: https://docs.usechar.com/guides/custom-styling Theme the Char agent with CSS variables for brand colors, typography, and dark mode Customize the agent appearance using CSS custom properties to match your application's design. ## How Theming Works The agent renders inside Shadow DOM, so normal CSS selectors can't reach it. Instead, set CSS custom properties (variables) on the \`\` element. ## Quick Theme Add CSS rules targeting the custom element: ```css theme={null} char { --char-color-primary: #0f766e; --char-color-primary-foreground: #ffffff; --char-color-background: #ffffff; --char-color-foreground: #0f172a; } ``` ## Common Customizations ### Brand Colors Match your primary action color: ```css theme={null} char { --char-color-primary: #7c3aed; /* Your brand color */ --char-color-primary-foreground: #fff; /* Text on primary */ } ``` ### Border Radius Control the roundness of UI elements: ```css theme={null} char { --char-radius: 0.5rem; /* Slightly rounded */ /* --char-radius: 0; Sharp corners */ /* --char-radius: 1rem; More rounded */ } ``` ### Message Bubbles Customize user and assistant message appearance: ```css theme={null} char { --char-user-bubble-bg: #7c3aed; --char-assistant-bubble-bg: #f3f4f6; } ``` ### Typography Use your application's font: ```css theme={null} char { --char-font-sans: "Inter", ui-sans-serif, system-ui, sans-serif; } ``` ## Dark Mode Scope variables to your existing dark mode selector: ```css theme={null} /* Light mode */ char { --char-color-background: #ffffff; --char-color-foreground: #0f172a; --char-color-muted: #f1f5f9; --char-color-border: #e2e8f0; } /* Dark mode */ html.dark char { --char-color-background: #0b1220; --char-color-foreground: #e2e8f0; --char-color-muted: #111827; --char-color-border: #1f2937; } ``` Adjust the selector (`html.dark`, `[data-theme="dark"]`, etc.) to match how your app handles dark mode. ## Complete Example Here's a full theme matching a teal brand: ```css theme={null} char { /* Brand */ --char-color-primary: #0f766e; --char-color-primary-foreground: #ffffff; /* Surfaces */ --char-color-background: #ffffff; --char-color-foreground: #0f172a; --char-color-muted: #f1f5f9; --char-color-border: #e2e8f0; /* Messages */ --char-user-bubble-bg: #0f766e; --char-assistant-bubble-bg: #f1f5f9; /* Shape & Typography */ --char-radius: 0.75rem; --char-font-sans: "Inter", system-ui, sans-serif; } ``` ## Reference For the complete list of CSS variables, see the [CSS Variables Reference](/reference/css-variables). # Embedding the Agent Source: https://docs.usechar.com/guides/embedding-agent Embed Char with the publishable-key auth contract in vanilla JS and modern frameworks Use this guide for the canonical embed contract. ## Auth contract (current) * `publishableKey`: **required** * `idToken`: optional `` handles ticket exchange internally through `POST /api/auth/pk-ticket`. ## Install ```bash theme={null} npm install @mcp-b/char ``` ```ts theme={null} import "@mcp-b/char/web-component"; ``` ```html theme={null} ``` ## Key-only embed ```html theme={null} ``` This gives org-scoped access without per-user identity. ## Per-user identity embed ```ts theme={null} import "@mcp-b/char/web-component"; import type { CharAgentElement } from "@mcp-b/char/web-component"; const agent = document.querySelector("char-agent") as CharAgentElement; const idToken = await getIdTokenForCurrentUser(); agent.connect({ publishableKey: "pk_live_...", idToken, }); ``` ## React (`@mcp-b/char-react`) ```tsx theme={null} import { CharAgent } from "@mcp-b/char-react"; export function SupportWidget({ idToken }: { idToken?: string }) { return ( ); } ``` ## Security notes * Keep `idToken` out of DOM attributes; pass it via `connect()`/`setAuth()`. * Treat publishable keys as public embed credentials and rotate if exposed unexpectedly. * Restrict key usage with allowed origins in dashboard settings. ## Troubleshooting * `MISSING_PUBLISHABLE_KEY`: provide `publishable-key` or `connect({ publishableKey })` * `INVALID_KEY`: key is revoked/invalid * `ORIGIN_NOT_ALLOWED`: add the origin to allowed origins/domains for that key * `INVALID_TOKEN`/`TOKEN_EXPIRED`: refresh user session and pass a fresh `idToken` # Testing Extension Tools Source: https://docs.usechar.com/guides/extension-testing Use Chrome DevTools MCP to test and debug extension tools from Claude Code **Private Beta** — [Contact us](mailto:alex@mcp-b.ai?subject=Extension%20Testing) to get set up with the Char Extension. The Char Extension exposes tools from web pages via content scripts. Chrome DevTools MCP provides two tools — `list_extension_tools` and `call_extension_tool` — that let you discover and call those tools directly from Claude Code. ## Prerequisites 1. **Chrome DevTools MCP** installed ([setup guide](/guides/mcp-servers)) 2. **Char Extension** installed and active 3. **Chrome launched with remote debugging**: ```bash theme={null} # macOS /Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --remote-debugging-port=9222 ``` ```bash theme={null} # Linux google-chrome --remote-debugging-port=9222 ``` ```bash theme={null} # Windows chrome.exe --remote-debugging-port=9222 ``` Chrome must be started with `--remote-debugging-port=9222` **before** any other Chrome windows are open. If Chrome is already running, quit it fully first. ## Install Chrome DevTools MCP If you're using the Char plugin, it's already configured. Otherwise install manually: ```bash theme={null} claude mcp add chrome-devtools npx -y @mcp-b/chrome-devtools-mcp@latest ``` Verify with `/mcp` — you should see `chrome-devtools` listed. Chrome DevTools MCP uses our maintained fork: [`@mcp-b/chrome-devtools-mcp`](https://www.npmjs.com/package/@mcp-b/chrome-devtools-mcp). Source: [WebMCP-org/npm-packages](https://github.com/WebMCP-org/npm-packages/tree/main/packages/chrome-devtools-mcp). ## Discover extension tools Navigate to a page that has WebMCP tools registered, then list what the extension sees: ```text theme={null} Use navigate_page to go to https://your-app.com Then use list_extension_tools ``` `list_extension_tools` queries the extension's service worker for all tools registered across all tabs. It returns tool names, descriptions, and input schemas. Use `summary: true` for a compact listing: ```text theme={null} Use list_extension_tools with summary: true ``` ## Call extension tools Once you know a tool name, call it: ```text theme={null} Use call_extension_tool with name "get_current_context" ``` Pass arguments as a JSON object: ```text theme={null} Use call_extension_tool with name "navigate" and arguments { "to": "/dashboard" } ``` ## How it works Both tools use `evaluate_in_extension_worker` internally — they run JavaScript in the extension's background service worker via Chrome DevTools Protocol. No separate MCP server or persistent connection is needed. ```mermaid theme={null} graph LR Claude[Claude Code] -->|CDP| SW[Extension Service Worker] SW -->|Content Script| Page[Web Page] Page -->|Tool Result| SW SW -->|CDP| Claude ``` Each call is stateless. The extension doesn't need any special configuration — `list_extension_tools` reads from the McpHub's in-memory tool registry, and `call_extension_tool` routes through the same content script bridge that the sidepanel uses. ## Extension tools vs WebMCP tools Chrome DevTools MCP has two parallel tool paths: | | Extension tools | WebMCP tools | | ----------------- | ------------------------------------------------ | --------------------------------------------- | | **List** | `list_extension_tools` | `list_webmcp_tools` | | **Call** | `call_extension_tool` | `call_webmcp_tool` | | **How it works** | Goes through the extension service worker | Injects directly into page via CDP | | **Bot detection** | Pages stay clean — no `navigator.webdriver` flag | CDP page attachment may trigger bot detection | | **Requires** | Char Extension installed | Nothing — works on any page | | **Tool source** | Content scripts + user scripts | `@mcp-b/global` polyfill on the page | Use **extension tools** when the Char Extension is active and you want clean, non-detectable tool execution. Use **WebMCP tools** when testing on pages without the extension. ## Debugging ### Check extension state Use `evaluate_in_extension_worker` to inspect the extension directly: ```text theme={null} Use evaluate_in_extension_worker with expression "globalThis.charExtension?.lifecycleState" ``` ```text theme={null} Use evaluate_in_extension_worker with expression "globalThis.charExtension?.hub?.getSerializableTools()?.length" ``` ### No tools found If `list_extension_tools` returns empty: 1. **Is the extension active?** Check `chrome://extensions` — the Char Extension should be enabled 2. **Is the page loaded?** Navigate to the page first, then wait for it to load 3. **Does the page have tools?** Not all pages register WebMCP tools. The page needs `@mcp-b/global` or a user script that calls `registerTool()` 4. **Content script connected?** Check the extension service worker console at `chrome://extensions` → Char → "Inspect views: service worker" ### Tool call fails If `call_extension_tool` returns an error: 1. **Check the tool name** — Use the exact name from `list_extension_tools`, not the prefixed MCP tool name 2. **Check arguments** — Use `list_extension_tools` (without `summary`) to see the full input schema 3. **Tab still open?** If the tab was closed, cached tools may fail. Navigate back to the page ## See also Extension overview Write scripts that inject tools Install Chrome DevTools MCP Tools in applications you control # User Scripts Source: https://docs.usechar.com/guides/extension-user-scripts Write scripts that inject tools into applications **Private Beta** — [Contact us](mailto:alex@mcp-b.ai?subject=Extension%20User%20Scripts) to get set up. User scripts inject tools into applications you don't control. They run in the context of a page and call `registerTool()` to expose functionality to the [Tool Hub](/explanation/tool-hub). ## The problem You want to add a tool to a legacy portal. The tool needs to read data from the page, click buttons, fill forms. But you can't modify the application's source code. ## How it works The [Char Extension](/guides/char-extension) injects scripts into matching pages via Chrome's user scripts API. The script runs in the page context—it can access the DOM, call page functions, interact with the application. ```mermaid theme={null} graph LR subgraph Page Script[User Script] end subgraph Extension SW[Service Worker] end Hub[Tool Hub] Script -->|registerTool| SW SW <-->|Uplink| Hub ``` When the script calls `registerTool()`, that tool becomes available through the Hub. AI agents can call it like any other tool. ## Org-level scripts Scripts are defined at the org level. An admin registers a script with domain matching rules. All users in the org get that script injected when visiting matching pages. This means consistent tooling across your organization. Everyone gets the same tools for the same legacy applications. No per-user configuration. ## `char.config.json` schema (`version: 2`) User scripts now use explicit script groups with native Chrome URL scoping fields. ```json theme={null} { "version": 2, "script_groups": [ { "group_id": "salesforce", "name": "Salesforce", "tool_prefix": "sf", "scope": { "match_patterns": ["https://*.my.salesforce.com/*"], "exclude_match_patterns": ["https://*.my.salesforce.com/setup/*"], "include_globs": ["*lightning*"], "all_frames": false, "run_at": "document_idle" }, "scripts": [ { "name": "lead_tools", "entry": "scripts/salesforce/lead-tools.ts", "description": "Lead tools for account pages" } ] } ] } ``` ### Scope resolution * Base scope comes from `group.scope`. * `script.scope_override` replaces only the fields it defines. * Effective defaults: * `all_frames`: `false` * `run_at`: `document_idle` ### Native-first matching The manifest maps directly to `chrome.userScripts.register`: * `match_patterns` -> `matches` * `exclude_match_patterns` -> `excludeMatches` * `include_globs` -> `includeGlobs` * `exclude_globs` -> `excludeGlobs` * `all_frames` -> `allFrames` * `run_at` -> `runAt` No custom runtime URL predicate engine is used in v2.

Tool prefix contract

Each script gets an effective prefix: * `group.tool_prefix + "_" + script.tool_prefix` (if script override exists) * Otherwise `group.tool_prefix + "_" + script.name` The extension prepends: `globalThis.__CHAR_TOOL_PREFIX__ = "";` Use that global in your script when naming tools to avoid collisions across sites/groups.

Manifest Field Reference

This section matches the fields shown in the dashboard User Scripts table. ### Script fields * `script_name`: Human-readable script name from config. * `script_id`: Stable identifier (`repo:group:script`) used for sync and dedupe. * `group_id`: Group key the script belongs to. * `tool_prefix`: Effective prefix exposed to the runtime (`__CHAR_TOOL_PREFIX__`).

Artifact Fields (artifact\_url, artifact\_path)

* `artifact_url`: Resolved source artifact URL (used for fetch and verification). * `artifact_path`: Normalized repository-relative path used for display and GitHub links.

Integrity Fields (artifact\_sha256, script\_sha)

* `artifact_sha256`: SHA-256 hash expected for downloaded artifact contents. * `script_sha`: Commit SHA associated with the script revision.

How Sync Status Is Computed

Dashboard `Sync Status` compares each `script_sha` to the repository `last_synced_sha`: * `Current`: `script_sha === last_synced_sha` * `Pending Sync`: `script_sha !== last_synced_sha`

Scope Field Reference

These fields map directly to native `chrome.userScripts.register` options.

match\_patterns

Primary URL match patterns for script injection. Must be non-empty after scope resolution.

exclude\_match\_patterns

Match patterns excluded from `match_patterns`.

include\_globs

Optional glob filters that further narrow matching URLs.

exclude\_globs

Optional glob filters that remove URLs after base matching.

all\_frames

When `true`, injects into matching iframes in addition to the top frame.

run\_at

Injection timing: * `document_start` * `document_end` * `document_idle` ## Development workflow Scripts are developed the same way as regular WebMCP tools—using [Chrome DevTools MCP](/guides/mcp-servers) to iterate in a live browser. ```mermaid theme={null} graph LR Claude[Claude Code] -->|Control browser| DevTools[Chrome DevTools MCP] DevTools -->|Test tools| Page[Legacy Portal] Page -->|Save script| Org[Org Script] ``` Navigate to the target application. Write `registerTool()` calls. Test them. When they work, save as an org-level script. ## What you get **Tools where you couldn't have them.** Legacy applications become AI-accessible without source code changes. **Shared across users.** Define once, deploy to everyone. Consistent tooling, centrally managed. **Same governance.** Tool calls from injected scripts flow through the same [governance](/guides/governance/index) layer—access control, guardrails, approvals, audit. ## See also The extension that runs user scripts Tools in applications you control # External MCP Servers Source: https://docs.usechar.com/guides/external-mcp-servers Connect third-party services through OAuth **Private Beta** — [Contact us](mailto:alex@mcp-b.ai?subject=External%20MCP%20Connectors) to get set up. External MCP servers are operated by third parties—SaaS vendors, public APIs, services outside your control. Users connect through OAuth, granting the [Tool Hub](/explanation/tool-hub) permission to call tools on their behalf. ## The difference from internal With [internal MCP servers](/guides/internal-mcp-servers), you control the code. You trust it. The identity exchange gives the server a scoped token, and you're confident it will use that token appropriately. External servers are different. You don't control the code or deployment. You don't know what the server does with the data it receives. Additional safeguards apply. ## How it works Users connect external services through OAuth. They see a consent screen, authorize access, and the Tool Hub stores the token. ```mermaid theme={null} sequenceDiagram participant User participant Hub as Tool Hub participant Ext as External MCP User->>Hub: Connect service Hub->>Ext: OAuth 2.1 + PKCE Ext-->>User: Consent screen User->>Ext: Authorize Ext-->>Hub: Access token Hub-->>User: Connected ``` When the user calls an external tool, the Hub uses that token. But it also inspects the request and response—[guardrails](/guides/governance/guardrails) run on external tool calls to catch prompt injection, PII leakage, and policy violations. ## What you get **User-consented access.** Users explicitly authorize each external service. They see what permissions they're granting. **Admin visibility.** You can require admin approval before users connect external services. No shadow IT—you know what's connected. **Content inspection.** External tool calls flow through guardrails. Arguments are scanned before sending; responses are scanned before returning. See [Trust Boundaries](/explanation/trust-boundaries) for how Char treats external tools differently. ## See also How Char classifies internal vs external tools # Angular Source: https://docs.usechar.com/guides/frameworks/angular Embed Char in Angular with publishable-key auth ```ts theme={null} import { Component, Input, ViewChild, ElementRef, AfterViewInit, CUSTOM_ELEMENTS_SCHEMA } from "@angular/core"; import "@mcp-b/char/web-component"; import type { CharAgentElement } from "@mcp-b/char/web-component"; @Component({ selector: "app-char-agent", standalone: true, schemas: [CUSTOM_ELEMENTS_SCHEMA], template: ``, }) export class CharAgentComponent implements AfterViewInit { @Input() idToken?: string; @ViewChild("agent") agentRef!: ElementRef; ngAfterViewInit() { if (!this.idToken) return; this.agentRef.nativeElement.connect({ publishableKey: "pk_live_...", idToken: this.idToken, }); } } ``` # Hotwire (Rails) Source: https://docs.usechar.com/guides/frameworks/hotwire Embed Char in Rails + Hotwire with persistent placement outside Turbo swaps Place `` in application layout outside Turbo frame swaps. ```erb theme={null} <%= yield %> ``` # HTMX Source: https://docs.usechar.com/guides/frameworks/htmx Embed Char in HTMX apps while preserving widget state across swaps Place the agent outside swap targets so it survives partial updates. ```html theme={null}
``` # Framework Guides Source: https://docs.usechar.com/guides/frameworks/index Framework-agnostic integration patterns for Char using publishableKey and optional idToken Char is a web component, so every framework follows the same auth model. ## Universal pattern 1. Render `` 2. If you have user auth, call `connect({ publishableKey, idToken })` 3. Never pass tokens in HTML attributes ```ts theme={null} agent.connect({ publishableKey: "pk_live_...", idToken, }); ``` Use `publishableKey` for every embed and pass `idToken` when user identity is available. ## Guides React and Next.js integration Vue and Nuxt integration Angular standalone/component patterns Svelte and SvelteKit integration Solid and SolidStart integration Partial-update integration Rails Turbo + Stimulus Laravel Livewire Phoenix LiveView ## Reference * [Embedding the Agent](/guides/embedding-agent) * [Identity Providers](/identity-providers/index) # LiveView (Phoenix) Source: https://docs.usechar.com/guides/frameworks/liveview Embed Char in Phoenix LiveView with a persistent root-level widget Use `phx-update="ignore"` so LiveView does not re-render the agent element. ```heex theme={null} <%= @inner_content %>
``` # Livewire (Laravel) Source: https://docs.usechar.com/guides/frameworks/livewire Embed Char in Livewire apps with persistent layout placement Render the agent in your app layout (outside Livewire component roots). ```blade theme={null} {{-- resources/views/layouts/app.blade.php --}} {{ $slot }} ``` # React Source: https://docs.usechar.com/guides/frameworks/react Embed Char in React apps with publishableKey and optional idToken ## Install ## Web component in React ```tsx theme={null} import { useEffect, useRef } from "react"; import "@mcp-b/char/web-component"; import type { CharAgentElement } from "@mcp-b/char/web-component"; const PUBLISHABLE_KEY = "pk_live_..."; export function SupportAgent({ idToken }: { idToken?: string }) { const ref = useRef(null); useEffect(() => { if (!idToken) return; ref.current?.connect({ publishableKey: PUBLISHABLE_KEY, idToken }); }, [idToken]); return ; } ``` ## React wrapper (`@mcp-b/char-react`) ```tsx theme={null} import { CharAgent } from "@mcp-b/char-react"; export function SupportAgent({ idToken }: { idToken?: string }) { return ; } ``` Use the same pattern in Next.js, Remix, and TanStack Start: render with `publishableKey`, pass `idToken` when available on the client. # Solid Source: https://docs.usechar.com/guides/frameworks/solid Embed Char in SolidJS and SolidStart ```tsx theme={null} import { onMount } from "solid-js"; import "@mcp-b/char/web-component"; import type { CharAgentElement } from "@mcp-b/char/web-component"; const PUBLISHABLE_KEY = "pk_live_..."; export function SupportAgent(props: { idToken?: string }) { let agentRef: CharAgentElement | undefined; onMount(() => { if (!props.idToken) return; agentRef?.connect({ publishableKey: PUBLISHABLE_KEY, idToken: props.idToken }); }); return ; } ``` # Svelte Source: https://docs.usechar.com/guides/frameworks/svelte Embed Char in Svelte and SvelteKit ```svelte theme={null} ``` # Vue Source: https://docs.usechar.com/guides/frameworks/vue Embed Char in Vue applications with publishable-key auth ```vue theme={null} ``` # Governance Source: https://docs.usechar.com/guides/governance/index BYO governance integrations for policy enforcement, approvals, and audit outcomes **Private Beta** — [Contact us](mailto:alex@mcp-b.ai?subject=Enterprise%20Governance) to get set up. Some deployments need more than tool access—they need controls. Who can use which tools, from where, under what conditions. What gets inspected. What requires approval. What gets logged. Char is provider-agnostic. You plug in your governance platform (LangGuard or BYO interceptor), and Char enforces its decisions uniformly across embedded agents, remote MCP servers, the browser extension, and autonomous execution. ## Authority model * **Identity provider (IdP)** is authoritative for identity, groups, and roles. * **Governance provider** is authoritative for tool policy decisions (`success`, `notify`, `failure`). * **Char UI hints** are informational only and are not enterprise enforcement controls. * **No managed policy engine** is required from Char for custom governance logic. ## Decision flow Every tool call flows through multiple layers: ```mermaid theme={null} graph TD subgraph "Identity" A[IdP Identity] --> B[Role / Group Claims] B --> C[App/Origin Constraints] end subgraph "Policy Provider" D[PDP Interceptor] end subgraph "Approval" E{Risk Class} --> F[Auto / Role / Explicit] end C --> D D --> E F --> G[Execute or Block] G --> H[Audit Log] ``` **Identity context** determines who is calling and what role context is attached. **Policy provider** evaluates content and context—is there PII in the arguments, does this look like prompt injection, does it violate org policies. **Approval** checks the risk classification—read-only operations auto-approve, destructive operations require explicit sign-off. **Audit** logs everything—who did what, when, with what arguments, what the result was. ## In this section LangGuard or bring your own policy provider Interceptor-based policy checks for MCP calls # Governance Integrations Source: https://docs.usechar.com/guides/governance/integrations Connect LangGuard or bring your own governance provider with the PDP interceptor contract. **Private Beta** — [Contact us](mailto:alex@mcp-b.ai?subject=Enterprise%20Governance) to get set up. Char governance is integration-based. You choose the policy provider. Char handles tool execution orchestration and propagates provider decisions to runtime metadata and UI. ## Trust boundaries * **IdP** provides authoritative identity, group, and role context. * **Governance provider** provides authoritative policy outcomes. * **UI hints and badges** are advisory only and should not be trusted for enterprise governance. ## Supported models | Model | Who owns policy logic | Integration path | | ---------------- | --------------------- | ----------------------------------- | | **LangGuard** | LangGuard | Configure interceptor URL + API key | | **BYO Provider** | Your team or vendor | Implement the interceptor contract | ## Integration contract Every provider must accept an `interceptor/validate` request and return: * `success` to allow tool execution * `notify` to allow execution with warnings * `failure` to block execution See [Policy Decision Point](/guides/governance/policy-decision-point) for the exact JSON-RPC request/response contract. ## Why this model * You keep governance vendor choice and can switch providers later. * Char does not require maintaining provider-specific policy engines. * Tool-call policy outcomes remain portable through shared metadata. * If no provider is configured or enabled, tool calls explicitly bypass governance checks. # Policy Decision Point Interceptor Source: https://docs.usechar.com/guides/governance/policy-decision-point Plug in LangGuard or your own PDP to allow, notify, or block MCP calls. **Private Beta** — [Contact us](mailto:alex@mcp-b.ai?subject=Enterprise%20Governance) to get set up. The Policy Decision Point (PDP) Interceptor extends MCP with a standardized webhook flow for evaluating MCP calls before they execute. The callee posts a JSON-RPC request to your interceptor service, which returns a validation result: **success**, **notify**, or **failure**. Char does not provide a managed policy engine for custom governance logic. Use LangGuard or any BYO service that implements this contract. ## Provider options * **LangGuard** for a ready-to-use external policy platform. * **BYO interceptor service** for internal or vendor governance engines. ## What is authoritative * Identity and role claims come from your IdP. * Policy decisions come from your configured interceptor provider. * UI-level warnings are not authoritative enforcement. ## When to use a PDP interceptor * Enforce enterprise policy beyond guardrails (e.g., geographic restrictions, tool allowlists). * Route sensitive requests to humans for approval. * Centralize policy decisions across multiple MCP servers. ## Configuration Configure the PDP provider per organization in **SaaS UI**: 1. Open `/beta/governance/integrations`. 2. Choose `LangGuard` or `Bring Your Own`. 3. Set `Interceptor URL`, `Interceptor Name`, and optional `API Key`. 4. Test the connection and save. 5. Enable the provider for active enforcement. If no provider is configured (or if the provider is disabled), tool calls bypass governance checks. For API-driven setup, use: * `GET /governance/providers/config` * `PATCH /governance/providers/config` * `POST /governance/providers/test` * `DELETE /governance/providers/config` MCP clients may discover interceptors via the standard `interceptors/list` JSON-RPC method: ```json theme={null} { "jsonrpc": "2.0", "id": 1, "method": "interceptors/list" } ``` Example response: ```json theme={null} { "jsonrpc": "2.0", "id": 1, "result": { "interceptors": [ { "name": "policy-byo", "version": "1.0.0", "description": "Acts as a Policy Decision Point (PDP) for MCP policy decisions", "events": ["tools/call"], "type": "policy-decision", "phase": "both", "compat": { "minProtocol": "2024-11-05" } } ] } } ``` ## Decision point request The callee sends a JSON-RPC request to the configured interceptor service URL. **HTTP** * **Method:** `POST` * **Content-Type:** `application/json` * **Authorization:** `Bearer ` **JSON-RPC envelope** ```json theme={null} { "jsonrpc": "2.0", "id": "call-12345", "method": "interceptor/validate", "params": { "interceptor_name": "policy-byo", "event": "tools/call", "phase": "request", "context": { "mcp_call_id": "call-12345", "caller_identity": "usr_abc123", "source": { "tabId": "12345", "origin": "https://example.com", "url": "https://example.com/page", "title": "Example Page" }, "mcp_call": { "method": "tools/call", "params": { "name": "write_note", "arguments": { "slug": "morning", "content": "Start your day with clarity and confidence." } } } } } } ``` ### Request fields | Field | Type | Description | | -------------------------------- | ------ | ----------------------------------------------------------- | | `jsonrpc` | string | Always `"2.0"`. | | `id` | string | Request identifier for correlation (same as `mcp_call_id`). | | `method` | string | Always `"interceptor/validate"`. | | `params.interceptor_name` | string | The configured interceptor name. | | `params.event` | string | The MCP event being intercepted (e.g., `"tools/call"`). | | `params.phase` | string | `"request"` or `"response"` depending on lifecycle. | | `params.context` | object | Original MCP call details used for policy evaluation. | | `params.context.mcp_call_id` | string | Unique identifier for the MCP call. | | `params.context.caller_identity` | string | Opaque user identifier (internal user ID). | | `params.context.source` | object | Browser context: `tabId`, `origin`, `url`, `title`. | | `params.context.mcp_call` | object | The original MCP method and parameters. | ## Decision point response The interceptor responds with a JSON-RPC `result` that contains a validation outcome. **HTTP Status Code:** `200 OK` (for a valid JSON-RPC response) ```json theme={null} { "jsonrpc": "2.0", "id": "call-12345", "result": { "status": "success", "message": "All policies passed.", "violations": [] } } ``` ### Response fields | Field | Type | Description | | ------------------- | ------ | ---------------------------------------------- | | `result.status` | string | `"success"`, `"notify"`, or `"failure"`. | | `result.message` | string | High-level description of the outcome. | | `result.violations` | array | List of policy violations (empty for success). | ### Violation object | Field | Type | Description | | ----------- | ------ | -------------------------------------------------- | | `policy_id` | string | Identifier of the violated policy. | | `status` | string | `"notify"` for escalation or `"block"` for denial. | | `details` | string | Optional human-readable message. | Example blocked response: ```json theme={null} { "jsonrpc": "2.0", "id": "call-12345", "result": { "status": "failure", "message": "The call was denied due to multiple policy violations.", "violations": [ { "policy_id": "POLICY_GEO_RESTRICTION", "status": "notify", "details": "Request originated from a restricted geographic area." }, { "policy_id": "POLICY_UNSANCTIONED_TOOL", "status": "block", "details": "The requested tool is not sanctioned for use in this context." } ] } } ``` ## Decision flow An MCP request arrives at the callee. Wrap the MCP call in the JSON-RPC `interceptor/validate` payload. POST the payload to your interceptor service URL. Verify the HTTP status is 200, then read `result.status` and `result.violations`. **Success:** proceed with the MCP call. **Notify:** log the notification for review and proceed with the MCP call. **Failure:** block the MCP call and log violations. ## Error handling * **Non-200 HTTP responses:** treat as a policy failure and block the request. * **Timeouts:** default to block and log for follow-up. * **Malformed JSON-RPC:** treat as failure and return an internal error to the caller. ## Security considerations * Authenticate the interceptor endpoint with a bearer token. * **Data exposure:** Tool call arguments are forwarded to the interceptor service for policy evaluation. Review your interceptor provider's data handling policies to ensure compliance with your organization's privacy requirements. * Log all validation outcomes to your audit system. * Keep a deny-by-default stance for transport or parsing errors. Need help wiring this into your MCP server? Reach out at [alex@mcp-b.ai](mailto:alex@mcp-b.ai). # Internal MCP Servers Source: https://docs.usechar.com/guides/internal-mcp-servers Connect internal services with identity-scoped access through Char **Private Beta** — [Contact us](mailto:alex@mcp-b.ai?subject=Internal%20MCP%20Connectors) to get set up. Internal MCP connectors let your users access internal tools with their own identity context. ## How identity reaches internal MCP ```mermaid theme={null} sequenceDiagram participant App as Your App participant IDP as Your IDP participant Agent as Embedded Agent participant Hub as Tool Hub participant MCP as Internal MCP App->>IDP: User authenticates IDP-->>App: ID Token App->>Agent: connect({ publishableKey, idToken }) Agent->>Hub: Connect with org + user identity Hub->>IDP: Token exchange IDP-->>Hub: Scoped assertion/token Hub->>MCP: tools/call with scoped token MCP-->>Hub: Result Hub-->>Agent: Result ``` ## Requirements * Publishable key configured for your org * IDP setup for token validation in Char * Remote MCP connector setup (beta) ## See also # Install MCP Servers (Claude Code Manual) Source: https://docs.usechar.com/guides/mcp-servers Manual Claude Code setup for Char MCP servers, with plugin-first fallback guidance. Use this page when you want a Claude Code-specific manual setup. Need setup for Codex, Gemini, Cursor, VS Code, or Windsurf? Use the canonical multi-client guide: [MCP Setup](/vibe/mcp-setup). ## Recommended Path (Plugin-First) If you install the Char plugin, setup is one command flow and all servers are added automatically. ```bash theme={null} /plugin marketplace add WebMCP-org/char-plugin /plugin install char@webmcp /char:setup ``` This auto-configures `chrome-devtools`, `char-docs`, `webmcp-docs`, and `char-saas`. ## Manual Path (Claude Code) Use this only if you are not using the plugin workflow. | Server | Purpose | | ------------------- | -------------------------------------------------------------------- | | **Chrome DevTools** | Browser automation—navigate pages, click elements, test WebMCP tools | | **Char Docs** | Access Char documentation from Claude Code | | **WebMCP Docs** | Access WebMCP API documentation | | **Char SaaS** | Manage your Char account—SSO config, org settings, browser tools | ### Install all servers ```bash theme={null} claude mcp add --transport http char-docs https://docs.usechar.ai/mcp claude mcp add --transport http webmcp-docs https://docs.mcp-b.ai/mcp claude mcp add --transport http char-saas https://app.usechar.ai/mcp claude mcp add chrome-devtools npx -y @mcp-b/chrome-devtools-mcp@latest ``` Chrome DevTools MCP uses our maintained fork package: [`@mcp-b/chrome-devtools-mcp`](https://www.npmjs.com/package/@mcp-b/chrome-devtools-mcp). Source: [WebMCP-org/npm-packages/packages/chrome-devtools-mcp](https://github.com/WebMCP-org/npm-packages/tree/main/packages/chrome-devtools-mcp). ### Verify installation ```text theme={null} /mcp ``` Confirm you see all four servers listed. ### First-time authentication (`char-saas`) The first time you use a Char SaaS tool, a browser window opens: 1. **Sign in** — Use your identity provider or create a new account 2. **Name your organization** — Char suggests a name based on your email domain 3. **Authorize** — Click "Authorize" and return to Claude Code This only happens once. Claude Code remembers your session. ## Troubleshooting (Manual Setup) ### MCP not appearing Restart Claude Code and run `/mcp` again. ### Permission errors Install Chrome DevTools MCP globally: ```bash theme={null} npm install -g @mcp-b/chrome-devtools-mcp claude mcp add chrome-devtools chrome-devtools-mcp ``` ### OAuth flow not completing 1. Check that you completed the sign-in flow 2. Verify the browser didn't block the redirect 3. Restart Claude Code and try the tool again ## See also * [MCP Setup](/vibe/mcp-setup) — Canonical setup guide for all supported clients * [Managing Char from Claude Code](/guides/char-saas-mcp) — Use the Char SaaS MCP tools * [MCP Tools Reference](/reference/mcp-tools) — Complete tool documentation # Private Beta Source: https://docs.usechar.com/guides/private-beta Features in active development with design partners These features are in private beta—actively developed with design partners before general release. Tell us about your use case and get early access ## Features Connect internal services with your existing identity infrastructure Unattended execution in cloud browsers Inject tools into applications you don't control Policy enforcement, guardrails, and audit trails Run Char entirely within your VPC ## Who we're looking for We're working with organizations that have: * **IT teams** responsible for internal tooling, APIs, and automation * **Compliance requirements** — audit trails, approval workflows, knowing who did what * **Legacy systems** — portals and applications you can't modify * **Interest in MCP** — connecting AI agents to internal services ## What you get * Early access before general release * Direct input into how features work * Support getting set up and integrated # Self-Hosted Deployment Source: https://docs.usechar.com/guides/self-hosted Run Char entirely within your VPC for strict data residency requirements For organizations with strict data residency requirements—healthcare, finance, government, manufacturing—Char is designed to be fully self-hostable. ## What This Means * **Run the entire Char Hub in your VPC** — No data leaves your network * **Use your internal inference endpoint** — vLLM, Azure OpenAI, Amazon Bedrock, or any OpenAI-compatible API * **Connect to internal MCP servers** — Behind your firewall, no tunnels required * **Same SDK, same tools** — Your frontend code and WebMCP tools work unchanged * **Built on Elixir/BEAM** — Battle-tested runtime for stateful, distributed systems ## Interested? Tell us about your data residency requirements We're working with design partners to shape the self-hosted experience. Early partners get input into the deployment architecture, priority access, and direct support. # VPC Connectivity Source: https://docs.usechar.com/guides/vpc-connectivity Connect to internal MCP servers without public exposure **Private Beta** — [Contact us](mailto:alex@mcp-b.ai?subject=VPC%20Connectivity) to get set up. Some MCP servers can't be exposed to the public internet. They live in VPCs, behind firewalls, in air-gapped environments. VPC connectivity lets the [Tool Hub](/explanation/tool-hub) reach these servers without requiring public endpoints. ## The problem Your internal services are internal for a reason. Exposing them to the internet—even with authentication—isn't acceptable. But the Tool Hub runs in our cloud. How does it reach your private network? ## How it works A Connector Worker deployed in your Cloudflare account bridges the gap. The Worker has access to your private network via Cloudflare Tunnel. The Tool Hub calls your Connector Worker over HTTPS; the Worker proxies requests through the tunnel to your internal MCP server. ```mermaid theme={null} graph LR subgraph Char Cloud Hub[Tool Hub] end subgraph Your Cloudflare Account CW[Connector Worker] Tunnel[Cloudflare Tunnel] end subgraph Your Private Network MCP[Internal MCP Server] end Hub -->|HTTPS| CW CW --> Tunnel Tunnel --> MCP ``` Your internal services stay private. The only public endpoint is the Connector Worker, which authenticates requests from the Tool Hub before forwarding them. ## What you get **No public exposure.** Your MCP servers never touch the public internet. Traffic flows through Cloudflare's network directly into your VPC. **Your infrastructure.** The Connector Worker runs in your Cloudflare account. You control the deployment, the tunnel configuration, the network policies. **Same identity model.** [ID-JAG](/reference/remote-mcp-auth) token exchange still works. The Connector Worker passes through the scoped credentials; your MCP server validates them as usual. ## See also Authentication for internal services # Add Custom Tools Source: https://docs.usechar.com/guides/webmcp-tools Register WebMCP tools to let the AI agent fill forms, click buttons, and interact with your UI Expose UI actions as tools the embedded agent can call. Register a tool, and the agent can fill forms, click buttons, read data—whatever you expose. ## Quick Example ```typescript theme={null} import '@mcp-b/global'; navigator.modelContext.registerTool({ name: 'add_to_cart', description: 'Add a product to the cart', inputSchema: { type: 'object', properties: { productId: { type: 'string' }, }, required: ['productId'], }, execute: async ({ productId }) => { document.querySelector(`[data-product="${productId}"] .add-btn`).click(); return { content: [{ type: 'text', text: 'Added to cart' }] }; }, }); ``` ## Full Documentation WebMCP has its own documentation site with complete API references, React hooks, live examples, and more. Full API reference, React hooks, and live examples ## Testing Your Tools The same tools you register are callable by Claude Code via Chrome DevTools MCP. This means the AI that writes your tools also tests them—Claude can navigate to your page, discover your tools, and call them directly. See [AI-Tested Tools](/explanation/ai-tested-tools) for why this matters. ## See also * [Install MCP Servers](/guides/mcp-servers) — Set up Chrome DevTools MCP * [AI-Tested Tools](/explanation/ai-tested-tools) — Why this workflow is powerful # Auth0 Source: https://docs.usechar.com/identity-providers/auth0 Configure Auth0 ID tokens for Char publishable-key embeds Use Auth0-issued ID tokens with Char. ## 1) Configure Auth0 in Char Set issuer/domain details and allowed audiences for your Auth0 application. ## 2) Get ID token from Auth0 SDK ```ts theme={null} const idToken = (await getIdTokenClaims())?.__raw; ``` ## 3) Connect ```ts theme={null} agent.connect({ publishableKey: "pk_live_...", idToken, }); ``` # AWS Cognito Source: https://docs.usechar.com/identity-providers/aws-cognito Configure Cognito ID tokens for Char publishable-key embeds Use Cognito User Pool ID tokens with Char. ## 1) Configure Cognito in Char Set issuer/domain details and allowed audiences. ## 2) Read token from Cognito session ```ts theme={null} const idToken = session.tokens?.idToken?.toString(); ``` ## 3) Connect ```ts theme={null} agent.connect({ publishableKey: "pk_live_...", idToken, }); ``` # Azure AD Source: https://docs.usechar.com/identity-providers/azure-ad Configure Microsoft Entra ID tokens for Char publishable-key embeds Use Microsoft Entra ID to issue user ID tokens for Char. ## 1) Configure provider in Char Set provider type, tenant/issuer settings, and allowed audiences in dashboard. ## 2) Read token from Entra auth flow ```ts theme={null} const idToken = tokenResponse?.idToken; ``` ## 3) Connect agent ```ts theme={null} agent.connect({ publishableKey: "pk_live_...", idToken, }); ``` # Better Auth Source: https://docs.usechar.com/identity-providers/better-auth Configure Better Auth tokens for Char publishable-key embeds Use Better Auth user tokens with Char. ## 1) Configure Better Auth in Char Set issuer and audience settings to match your Better Auth OIDC/JWT configuration. ## 2) Read token from your Better Auth session ```ts theme={null} const idToken = await getIdTokenFromSession(); ``` ## 3) Connect ```ts theme={null} agent.connect({ publishableKey: "pk_live_...", idToken, }); ``` # Clerk Source: https://docs.usechar.com/identity-providers/clerk Configure Clerk user tokens for Char publishable-key embeds Use Clerk session tokens with Char. ## 1) Configure Clerk in Char Set provider/issuer settings and allowed audiences. ## 2) Read token from Clerk ```ts theme={null} const idToken = await getToken(); ``` ## 3) Connect ```ts theme={null} agent.connect({ publishableKey: "pk_live_...", idToken, }); ``` # Custom OIDC Source: https://docs.usechar.com/identity-providers/custom-oidc Configure any OIDC provider for Char publishable-key embeds Use any OpenID Connect provider that issues standards-compliant ID tokens. ## Requirements * Valid issuer URL * Discovery/JWKS endpoint * Audience values configured in Char ## Integration pattern ```ts theme={null} const idToken = await getIdTokenFromYourProvider(); agent.connect({ publishableKey: "pk_live_...", idToken, }); ``` ## Validation checks Char validates: * Signature via JWKS * Issuer match * Audience match * Expiration # Firebase Auth Source: https://docs.usechar.com/identity-providers/firebase Configure Firebase ID tokens for Char publishable-key embeds Use Firebase Authentication ID tokens with Char. ## 1) Configure Firebase in Char Set issuer and audience settings that match your Firebase project. ## 2) Read token from Firebase user ```ts theme={null} const idToken = await user.getIdToken(); ``` ## 3) Connect ```ts theme={null} agent.connect({ publishableKey: "pk_live_...", idToken, }); ``` # Google Workspace Source: https://docs.usechar.com/identity-providers/google Configure Google ID tokens for Char publishable-key embeds Use Google Identity Services (or Firebase-authenticated Google users) to pass ID tokens to Char. ## 1) Configure Google in Char Set provider settings and allowed audiences in dashboard. ## 2) Read token ```ts theme={null} const idToken = response.credential; ``` ## 3) Connect ```ts theme={null} agent.connect({ publishableKey: "pk_live_...", idToken, }); ``` # Identity Providers Source: https://docs.usechar.com/identity-providers/index Configure ID token validation for Char with publishable-key embedding Char uses your existing IDP for end-user identity. ## Current embed contract * `publishableKey` is required on every connection * `idToken` is optional and enables per-user identity * Audience validation uses your org's configured allowed audiences ```ts theme={null} agent.connect({ publishableKey: "pk_live_...", idToken: currentUser.idToken, }); ``` ## Setup flow In Char dashboard, create a publishable key and optionally restrict allowed origins. Set issuer/domain and allowed audiences so Char can validate incoming `idToken` values. Use `connect({ publishableKey, idToken })` (or `setAuth`) in your app. ## Supported providers ## Security model | Control | Behavior | | -------------------- | ----------------------------------------------------------------------- | | Issuer validation | `iss` must match your configured provider issuer | | Signature validation | Tokens validated via provider JWKS | | Audience validation | `aud` must match configured allowed audiences | | Origin checks | Publishable key origin restrictions are enforced before ticket issuance | # Keycloak Source: https://docs.usechar.com/identity-providers/keycloak Configure Keycloak tokens for Char publishable-key embeds Use Keycloak ID tokens with Char. ## 1) Configure Keycloak in Char Set issuer/domain and allowed audience values for your realm/application. ## 2) Read token from Keycloak client ```ts theme={null} const idToken = keycloak.idToken; ``` ## 3) Connect ```ts theme={null} agent.connect({ publishableKey: "pk_live_...", idToken, }); ``` # Logto Source: https://docs.usechar.com/identity-providers/logto Configure Logto tokens for Char publishable-key embeds Use Logto tokens with Char. ## 1) Configure Logto in Char Add issuer/domain and allowed audience settings. ## 2) Read token from Logto SDK ```ts theme={null} const idToken = (await getAccessTokenClaims())?.__raw; ``` ## 3) Connect ```ts theme={null} agent.connect({ publishableKey: "pk_live_...", idToken, }); ``` # Okta Source: https://docs.usechar.com/identity-providers/okta Configure Okta ID tokens for Char publishable-key embeds Use Okta to issue user ID tokens, then pass them to Char with your publishable key. ## 1) Configure Okta in Char In Char dashboard, set: * Provider type: `okta` * Domain/issuer values for your Okta org * Allowed audiences used by your app ## 2) Read token from Okta SDK ```ts theme={null} const idToken = authState?.idToken?.idToken; ``` ## 3) Connect agent ```ts theme={null} agent.connect({ publishableKey: "pk_live_...", idToken, }); ``` # OneLogin Source: https://docs.usechar.com/identity-providers/onelogin Configure OneLogin tokens for Char publishable-key embeds Use OneLogin ID tokens with Char. ## 1) Configure OneLogin in Char Set issuer/domain and audience settings. ## 2) Read token from your OneLogin auth client ```ts theme={null} const idToken = user.id_token; ``` ## 3) Connect ```ts theme={null} agent.connect({ publishableKey: "pk_live_...", idToken, }); ``` # Ping Identity Source: https://docs.usechar.com/identity-providers/ping-identity Configure PingOne/PingFederate tokens for Char publishable-key embeds Use Ping-issued ID tokens with Char. ## 1) Configure Ping in Char Set issuer/domain and allowed audience settings. ## 2) Read token from Ping auth result ```ts theme={null} const idToken = user.id_token; ``` ## 3) Connect ```ts theme={null} agent.connect({ publishableKey: "pk_live_...", idToken, }); ``` # Scalekit Source: https://docs.usechar.com/identity-providers/scalekit Configure Scalekit tokens for Char publishable-key embeds Use Scalekit-issued user tokens with Char. ## 1) Configure Scalekit in Char Add issuer settings and allowed audiences. ## 2) Read token from Scalekit auth state ```ts theme={null} const { idToken } = useScalekitAuth(); ``` ## 3) Connect ```ts theme={null} agent.connect({ publishableKey: "pk_live_...", idToken, }); ``` # WorkOS Source: https://docs.usechar.com/identity-providers/workos Configure WorkOS user tokens for Char publishable-key embeds Use WorkOS-authenticated user tokens with Char. Ensure your token audience configuration matches what you enter in Char allowed audiences. ## 1) Configure WorkOS in Char Enter issuer/domain settings and allowed audiences. ## 2) Get token from WorkOS auth layer ```ts theme={null} const idToken = await getWorkOSIdToken(); ``` ## 3) Connect ```ts theme={null} agent.connect({ publishableKey: "pk_live_...", idToken, }); ``` # Introduction Source: https://docs.usechar.com/index Char is an embedded AI agent platform with cross-app tools, SSO authentication, and WebMCP browser automation Char Hero Char Hero Char is an embedded AI agent that can interact with your web application. Users chat with the agent, and it uses WebMCP tools to fill forms, click buttons, read data, and navigate—all within your UI. ## What Char provides * **Embeddable agent** — Drop-in web component for any framework * **WebMCP tools** — Let the agent interact with your UI * **SSO authentication** — Use your existing identity provider * **Organization skills** — Guide agent behavior with SKILL.md files * **Cross-app tools** — Access tools from multiple browser tabs and remote services ## AI-Tested Tools The AI that writes your tools also tests them. Chrome DevTools MCP lets Claude Code call the exact same WebMCP tools that your embedded agent uses. When Claude writes a tool, it tests it immediately. If Claude can't use a tool, it fixes it—before your users ever see it. Why this changes everything ## Get Started Two commands. Claude handles the rest. Step-by-step setup by hand. MCP servers, skills, llms.txt, and source links. ## How-to Guides Task-oriented guides for users who already have Char running. Add custom UI actions Guide agent behavior Theme the agent ## Identity Providers Auth0, Okta, Azure AD, Google, and more Connect internal MCP servers with your IDP (Private Beta) ## Explanation Understanding-oriented content for deeper knowledge. Architecture overview Cross-app tool aggregation ## Reference Configuration options Char SaaS MCP reference REST endpoints # Get Started Manually Source: https://docs.usechar.com/quickstart Install and embed the Char agent using publishable-key authentication This tutorial walks through a production-ready Char embed setup using a publishable key. **Prefer automation?** See [Get Started with Claude Code](/guides/claude-code-setup). ## What you'll build By the end, you'll have: * A working `` embed * Org-scoped authentication via a publishable key * Optional per-user identity with `idToken` ## Prerequisites * A Char account and organization * A publishable key from **Dashboard → Settings → Publishable Keys** * A web app (or static HTML page) ## Step 1: Install ## Step 2: Import the component ```ts theme={null} import "@mcp-b/char/web-component"; ``` ## Step 3: Add `` with your publishable key ```html theme={null} ``` This is enough to run the agent in key-only mode. ## Step 4: Add per-user identity (optional) If your users already authenticate with an IDP, pass their ID token with `connect()`: ```ts theme={null} import type { CharAgentElement } from "@mcp-b/char/web-component"; const agent = document.querySelector("char-agent") as CharAgentElement; const idToken = await getIdTokenForCurrentUser(); agent.connect({ publishableKey: "pk_live_...", idToken, }); ``` `publishableKey` is required. `idToken` is optional. ## Step 5: Verify 1. Start your app 2. Open the page with `` 3. Open the agent and send a message If you can chat, the embed is working. ## Next steps Full auth and integration patterns Configure ID token validation React, Vue, Angular, and more Match the agent to your product # Agent Attributes Source: https://docs.usechar.com/reference/agent-attributes Reference for attributes, properties, and auth methods Reference for the `` custom element. ## Attributes | Attribute | Type | Description | | -------------------- | ----------------------------------- | ------------------------------------------- | | `publishable-key` | `string` | Publishable key used to resolve org context | | `display-mode` | `'pip' \| 'inline' \| 'fullscreen'` | Host display mode hint | | `enable-debug-tools` | `boolean` | Enables debug tools in embed runtime | | `api-base` | `string` | Overrides API base (default production) | ## Properties | Property | Type | Description | | ---------------- | --------------------- | ---------------------------------- | | `publishableKey` | `string \| undefined` | Property form of `publishable-key` | | `apiBase` | `string \| undefined` | Property form of `api-base` | ## Imperative auth API ```ts theme={null} import type { CharAgentElement } from "@mcp-b/char/web-component"; const agent = document.querySelector("char-agent") as CharAgentElement; agent.connect({ publishableKey: "pk_live_...", idToken: currentUserIdToken, // optional }); ``` ### `connect(options)` * Requires `publishableKey` * Accepts optional `idToken` * Returns `boolean` ### `setAuth(options | null)` * Wrapper for `connect()` / `disconnect()` * Pass `null` to clear auth ### `disconnect()` * Clears auth in the embed runtime * Returns `boolean` ## Method option types | Option | Type | Required | Notes | | ---------------- | -------- | -------- | ------------------------------------- | | `publishableKey` | `string` | Yes | Org-scoped embed credential | | `idToken` | `string` | No | End-user identity token from your IDP | Only `publishableKey` (required) and `idToken` (optional) are supported in auth methods. ## Events | Event | Detail | | --------------------------- | ------------------------------ | | `char-initialized` | `void` | | `char-close` | `void` | | `char-error` | `CharErrorDetail` | | `char-size-changed` | `CharSizeChangedDetail` | | `char-request-display-mode` | `CharRequestDisplayModeDetail` | | `char-open-link` | `CharOpenLinkDetail` | # CSS Variables Source: https://docs.usechar.com/reference/css-variables Complete CSS custom properties reference for theming the Char agent The \`\` component renders inside Shadow DOM, so external selectors cannot reach its internals. Customize the UI by setting CSS custom properties on the host element. ## Quick Example ```css theme={null} char { --char-color-primary: #0f766e; --char-color-primary-foreground: #ffffff; --char-color-background: #ffffff; --char-color-foreground: #0f172a; } ``` ## Tier 1: Brand Matching These are the essential variables for matching your brand: | Variable | Description | | --------------------------------- | -------------------------------------------------- | | `--char-color-primary` | Primary action color (send button, links, accents) | | `--char-color-primary-foreground` | Text on primary surfaces | | `--char-color-background` | Main background color | | `--char-color-foreground` | Primary text color | | `--char-radius` | Border radius (e.g., `0.75rem`, `0`, `1rem`) | | `--char-font-sans` | Sans-serif font stack | ## Tier 2: Deeper Customization For more granular control over specific elements: | Variable | Description | | ---------------------------- | ----------------------------- | | `--char-color-muted` | Muted/secondary surface color | | `--char-color-border` | Borders and dividers | | `--char-user-bubble-bg` | User message background | | `--char-assistant-bubble-bg` | Assistant message background | ## Dark Mode Scope variables to your existing dark mode selector: ```css theme={null} /* Light mode (default) */ char { --char-color-background: #ffffff; --char-color-foreground: #0f172a; --char-color-muted: #f1f5f9; --char-color-border: #e2e8f0; } /* Dark mode */ html.dark char, [data-theme="dark"] char { --char-color-background: #0b1220; --char-color-foreground: #e2e8f0; --char-color-muted: #111827; --char-color-border: #1f2937; } ``` # MCP Tools Reference Source: https://docs.usechar.com/reference/mcp-tools Complete reference for Char SaaS MCP tools The Char SaaS MCP exposes these tools at `https://app.usechar.ai/mcp`. ## Profile ### get\_profile Returns the authenticated user's profile and organization memberships. **Input**: None **Output**: ```json theme={null} { "id": "user_01ABC...", "email": "user@example.com", "name": "Jane Doe", "organizations": [ { "id": "org_01ABC...", "name": "Acme Corp", "slug": "acme-corp", "role": "admin" } ] } ``` *** ## Organizations ### list\_organizations Returns all organizations the authenticated user belongs to. **Input**: None **Output**: ```json theme={null} { "organizations": [ { "id": "org_01ABC...", "name": "Acme Corp", "slug": "acme-corp", "role": "admin" } ], "count": 1 } ``` ### get\_organization Returns details of an organization by ID or slug. **Input**: | Parameter | Type | Required | Description | | --------- | ------ | -------- | -------------------------------------- | | `id` | string | No | Organization ID (e.g., `org_01ABC...`) | | `slug` | string | No | Organization slug | Provide either `id` or `slug`, not both. **Output**: ```json theme={null} { "id": "org_01ABC...", "name": "Acme Corp", "slug": "acme-corp", "role": "admin" } ``` ### list\_members Returns all members of the current organization. Requires organization context. **Input**: None **Output**: ```json theme={null} { "members": [ { "user_id": "user_01ABC...", "email": "user@example.com", "name": "Jane Doe", "role": "admin", "joined_at": "2024-01-15T10:30:00Z" } ], "count": 1 } ``` *** ## Identity ### manage\_idp\_config Gets, updates, or clears the identity provider configuration for SSO. Requires organization context and admin role. **Input**: | Parameter | Type | Required | Description | | ----------------- | ------------------------------------------------------------------- | -------- | ------------------------------------------------------- | | `action` | `"get"` \| `"update"` \| `"clear"` | Yes | Action to perform | | `idp_type` | `"okta"` \| `"azure"` \| `"auth0"` \| `"google"` \| `"custom_oidc"` | No | Identity provider type (for update) | | `idp_client_id` | string | No | OIDC client ID (for update) | | `idp_domain` | string | No | Provider domain (for update) | | `idp_issuer` | string | No | Full issuer URL (for update, required for custom\_oidc) | | `allowed_domains` | string\[] | No | Allowed origins for agent embedding (for update) | **Output**: ```json theme={null} { "idp_type": "okta", "idp_client_id": "0oa1234...", "idp_domain": "acme.okta.com", "idp_issuer": null, "allowed_domains": ["https://app.example.com"] } ``` ### test\_idp\_connection Tests connectivity to an identity provider by fetching its OIDC discovery document. **Input**: | Parameter | Type | Required | Description | | ------------ | ------------------------------------------------------------------- | -------- | ------------------------------------------------- | | `idp_type` | `"okta"` \| `"azure"` \| `"auth0"` \| `"google"` \| `"custom_oidc"` | Yes | Identity provider type | | `idp_domain` | string | No | Provider domain (required for okta, azure, auth0) | | `idp_issuer` | string | No | Full issuer URL (required for custom\_oidc) | **Output**: ```json theme={null} { "success": true, "issuer": "https://acme.okta.com", "jwks_uri": "https://acme.okta.com/oauth2/v1/keys" } ``` On failure: ```json theme={null} { "success": false, "error": "Failed to fetch discovery document" } ``` *** ## Governance ### manage\_governance\_provider Gets, updates, or clears the organization governance provider configuration used for policy decisions. Requires organization context and admin role. **Input**: | Parameter | Type | Required | Description | | ------------------ | ---------------------------------- | -------- | ------------------------------------------- | | `action` | `"get"` \| `"update"` \| `"clear"` | Yes | Action to perform | | `provider_key` | `"langguard"` \| `"byo"` | No | Provider key (for update) | | `interceptor_url` | string (URL) | No | HTTPS interceptor endpoint (for update) | | `interceptor_name` | string | No | Interceptor identifier (for update) | | `api_key` | string | No | Optional API key to set/rotate (for update) | | `is_enabled` | boolean | No | Enable/disable policy checks (for update) | For `update`, omitted fields are merged with the current saved configuration. **Output**: ```json theme={null} { "action": "update", "effective_status": "active", "reason": "Provider is enabled and policy checks are active.", "config": { "provider_key": "langguard", "interceptor_url": "https://api.langguard.ai/interceptor", "interceptor_name": "policy-langguard.ai", "api_key_last4": "abcd", "has_api_key": true, "is_enabled": true, "last_test_status": "success", "last_test_message": "All policies passed.", "last_test_at": "2026-02-06T12:34:56.000Z", "created_at": "2026-02-06T12:30:00.000Z", "updated_at": "2026-02-06T12:34:56.000Z" } } ``` ### test\_governance\_provider Tests governance provider connectivity and interceptor response contract. Requires organization context and admin role. **Input**: | Parameter | Type | Required | Description | | ------------------ | ------------------------ | -------- | ----------------------------------- | | `provider_key` | `"langguard"` \| `"byo"` | No | Provider key override for this test | | `interceptor_url` | string (URL) | No | HTTPS interceptor endpoint override | | `interceptor_name` | string | No | Interceptor identifier override | | `api_key` | string | No | API key override for this test | When omitted, values are taken from the currently saved governance configuration. **Output**: ```json theme={null} { "provider_key": "langguard", "interceptor_url": "https://api.langguard.ai/interceptor", "interceptor_name": "policy-langguard.ai", "status": "notify", "message": "1 policy warning detected. Review recommended.", "violations": [ { "policy_id": "POLICY_DATA_CLASSIFICATION", "status": "notify", "details": "Trace is missing required data classification metadata" } ], "latency_ms": 1320 } ``` *** ## Plans ### list\_plans Returns all available plan tiers. **Input**: None **Output**: ```json theme={null} { "plans": [ { "id": "plan_free", "name": "Free", "description": "For individuals and small teams", "features": {...}, "limits": {...}, "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z" } ], "count": 3 } ``` ### get\_plan\_info Returns the current organization's plan, features, and usage. Requires organization context. **Input**: | Parameter | Type | Required | Description | | --------- | ------------------------------------------------- | -------- | ---------------------------------- | | `include` | `"basic"` \| `"features"` \| `"usage"` \| `"all"` | No | What to include (default: `"all"`) | **Output** (with `include: "all"`): ```json theme={null} { "plan": { "id": "plan_pro", "name": "Pro", "description": "For growing teams" }, "features": { "sso": true, "custom_domains": false }, "limits": { "seats": 10, "end_users": 1000 }, "usage": { "seats_used": 3, "end_users_used": 150 } } ``` ### check\_feature Checks if a specific feature is accessible for the current organization. **Input**: | Parameter | Type | Required | Description | | --------------- | ----------------------------------------------------------------------------------------- | -------- | ------------------------------------------ | | `feature` | `"sso"` \| `"scim"` \| `"audit_logs"` \| `"custom_domains"` \| `"end_users"` \| `"seats"` | Yes | Feature to check | | `current_usage` | number | No | Current usage count (for limited features) | **Output**: ```json theme={null} { "feature": "sso", "allowed": true, "reason": null } ``` On denial: ```json theme={null} { "feature": "custom_domains", "allowed": false, "reason": "Requires Pro plan or higher" } ``` *** ## Subscriptions ### get\_subscription Returns the current organization's subscription information. **Input**: | Parameter | Type | Required | Description | | --------- | ------------------------------------------ | -------- | ------------------------------------- | | `detail` | `"active_check"` \| `"status"` \| `"full"` | No | Level of detail (default: `"status"`) | **Output** (with `detail: "status"`): ```json theme={null} { "has_subscription": true, "is_active": true, "status": "active", "plan_id": "plan_pro", "current_period_end": "2024-02-15T00:00:00Z", "cancel_at": null } ``` ### get\_billing\_url Returns a checkout or customer portal URL. **Input**: | Parameter | Type | Required | Description | | ------------ | -------------------------- | -------- | ---------------------------------------- | | `type` | `"checkout"` \| `"portal"` | Yes | URL type | | `product_id` | string | No | Polar product ID (required for checkout) | **Output**: ```json theme={null} { "url_type": "portal", "url": "https://polar.sh/..." } ``` ### sync\_subscription Forces a sync of subscription data from Polar. **Input**: None **Output**: ```json theme={null} { "synced": 1, "deleted": 0 } ``` *** ## Hub ### hub\_list\_tools Lists browser tools available from connected WebMCP tabs. **Input**: | Parameter | Type | Required | Description | | ----------------- | ------- | -------- | -------------------------------------------------------- | | `search` | string | No | Filter tools by name or description | | `source_id` | string | No | Filter to tools from a specific source/tab | | `include_sources` | boolean | No | Include source metadata for each tool (default: `false`) | **Output**: ```json theme={null} { "count": 2, "tools": [ { "name": "submit_form", "description": "Submit the contact form", "inputSchema": {...} } ] } ``` ### hub\_list\_sources Lists active browser tabs/sources that have registered tools. **Input**: None **Output**: ```json theme={null} { "count": 1, "sources": [ { "sourceId": "src_01ABC...", "tabId": "tab_123", "origin": "https://app.example.com", "title": "My App", "url": "https://app.example.com/dashboard", "toolCount": 3, "connectedAt": "2024-01-15T10:30:00Z", "lastSeenAt": "2024-01-15T10:35:00Z" } ] } ``` ### hub\_call\_tool Executes a browser tool by name. **Input**: | Parameter | Type | Required | Description | | ----------- | ------ | -------- | ------------------------------------------------------------------------ | | `tool_name` | string | Yes | Name of the tool to call | | `args` | object | No | Arguments to pass to the tool | | `source_id` | string | No | Specific source/tab ID (required if multiple sources have the same tool) | **Output**: Varies by tool. Returns the tool's response content. *** ## Utility ### echo Echoes back a message. Useful for testing the MCP connection. **Input**: | Parameter | Type | Required | Description | | --------- | ------ | -------- | ----------------------------------- | | `message` | string | Yes | Message to echo (1-1000 characters) | **Output**: ```json theme={null} { "message": "Echo: Hello, world!" } ``` ### verify\_api\_access Verifies the current session has valid API access. **Input**: None **Output**: ```json theme={null} { "status": "ok", "message": "Session verified with organization context", "authenticated": true, "user_email": "user@example.com", "organization_id": "org_01ABC...", "organization_name": "Acme Corp", "role": "admin", "token_expires_at": "2024-01-15T11:30:00Z", "token_expiring_soon": false, "has_refresh_token": true } ``` *** ## See also * [Managing Char from Claude Code](/guides/char-saas-mcp) * [Install MCP Servers](/guides/mcp-servers) # Remote MCP Authentication Source: https://docs.usechar.com/reference/remote-mcp-auth Technical specification for ID-JAG authentication between the Tool Hub and Remote MCP servers **Private Beta** — Remote MCP authentication is currently in private beta. [Contact us](mailto:alex@mcp-b.ai?subject=Remote%20MCP%20Access) to get access. This reference describes the ID-JAG authentication flow. For conceptual understanding of why identity delegation matters, see [Why Char Exists](/explanation/how-char-works) and [Federated Authentication](/explanation/federated-auth). **Prerequisites:** ID-JAG requires [one-time IDP setup](/explanation/enterprise-sso-mcp#setup-requirements). Your IDP admin must register Char as an OAuth client and grant token exchange permissions before the flows described here will work. ## Standards ID-JAG (Identity Assertion Authorization Grant) uses four OAuth/OIDC specifications: | Standard | Role in ID-JAG | | --------------------------------------------------------- | ---------------------------------------------------------------------- | | [RFC 8693](https://datatracker.ietf.org/doc/html/rfc8693) | Token Exchange - Hub obtains ID-JAG from IDP | | [RFC 7523](https://datatracker.ietf.org/doc/html/rfc7523) | JWT Bearer Grant - Hub exchanges ID-JAG for access token at MCP server | | [RFC 8707](https://datatracker.ietf.org/doc/html/rfc8707) | Resource Indicators - scopes tokens to specific MCP servers | | [RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728) | Protected Resource Metadata - MCP servers advertise auth requirements | ## Authentication Flow ```mermaid theme={null} sequenceDiagram participant User as User Browser participant IDP as Identity Provider participant Hub as Tool Hub participant MCP as Remote MCP Server User->>IDP: Authenticate IDP-->>User: ID Token (aud: frontend) User->>Hub: Connect with ID Token Hub->>Hub: Validate token against IDP JWKS Note over Hub,MCP: Tool invocation Hub->>IDP: Token Exchange (RFC 8693) IDP-->>Hub: ID-JAG (aud: mcp-server) Hub->>MCP: JWT Bearer Grant (RFC 7523) MCP-->>Hub: Access Token Hub->>MCP: tools/call + Bearer Token MCP-->>Hub: Tool result ``` ### Step 1: Token Exchange Request The Hub requests an ID-JAG from the IDP: ```http theme={null} POST /oauth/token HTTP/1.1 Host: your-idp.example.com Content-Type: application/x-www-form-urlencoded grant_type=urn:ietf:params:oauth:grant-type:token-exchange &subject_token={user_id_token} &subject_token_type=urn:ietf:params:oauth:token-type:id_token &requested_token_type=urn:ietf:params:oauth:token-type:id-jag &audience=https://mcp-server.internal.com &resource=https://mcp-server.internal.com &scope=mcp:tools:read mcp:tools:call ``` ### Step 2: ID-JAG Structure The IDP returns a JWT with type `oauth-id-jag+jwt`: ```json theme={null} // Header { "typ": "oauth-id-jag+jwt", "alg": "RS256", "kid": "idp-signing-key-id" } // Payload { "iss": "https://sso.enterprise.com", "sub": "user@enterprise.com", "aud": "https://mcp-server.internal.com", "client_id": "char-tool-hub", "resource": "https://mcp-server.internal.com", "scope": "mcp:tools:read mcp:tools:call", "exp": 1737100000, "iat": 1737099700, "jti": "unique-assertion-id" } ``` ### Step 3: JWT Bearer Grant The Hub exchanges the ID-JAG for an access token at the MCP server: ```http theme={null} POST /oauth/token HTTP/1.1 Host: mcp-server.internal.com Content-Type: application/x-www-form-urlencoded grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer &assertion={id_jag_jwt} &client_id=char-tool-hub &client_secret={tool_hub_secret} ``` ### Step 4: Access Token Response ```json theme={null} { "access_token": "eyJhbGciOiJSUzI1NiIs...", "token_type": "Bearer", "expires_in": 3600 } ``` ## IDP Requirements ID-JAG authentication requires your identity provider to support: 1. **RFC 8693 Token Exchange** — The ability to exchange tokens at the `/token` endpoint 2. **ID-JAG Token Type** — Issuing JWTs with `typ: "oauth-id-jag+jwt"` and accepting `requested_token_type=urn:ietf:params:oauth:token-type:id-jag` Many providers support the underlying Token Exchange primitive but not the specific ID-JAG profile yet. | Requirement | Notes | | ----------------------------- | ------------------------------- | | Token Exchange (RFC 8693) | Required | | ID-JAG Token Type | Required for full compatibility | | Configurable resource servers | For audience scoping | | JWKS endpoint | For signature validation | ### IDP Support Matrix ID-JAG requires both RFC 8693 (Token Exchange) and support for the `oauth-id-jag+jwt` token type. Support varies significantly across providers. | Provider | RFC 8693 | ID-JAG Token Type | Status | Tracking | | ---------------- | ---------- | ----------------- | ---------------------------------------------------------------- | ----------------------------------------------------------- | | Okta | ✅ | ⚠️ Early Access | [XAA Early Access](https://www.okta.com/saas-security/sign-up/) | Active development | | PingFederate | ✅ | ❓ Unconfirmed | Full RFC 8693, ID-JAG unverified | — | | PingOne | ✅ | ❓ Unconfirmed | Full RFC 8693, ID-JAG unverified | — | | Auth0 | ✅ | ⚠️ Beta | [XAA Resource App Beta](https://auth0.com/docs/xaa-resource-app) | Active development | | Keycloak | ✅ (v26.2+) | ❌ Not yet | Token Exchange GA, ID-JAG pending | [#43971](https://github.com/keycloak/keycloak/issues/43971) | | Azure AD / Entra | ❌ | ❌ | Uses proprietary On-Behalf-Of | — | | AWS Cognito | ❌ | ❌ | No RFC 8693 support | — | | Google Workspace | ⚠️ Limited | ❌ | Workload Identity Federation only | — | **Legend:** ✅ = Supported, ⚠️ = Partial/Early Access, ❌ = Not supported, ❓ = Unconfirmed This matrix reflects the state as of January 2026. ID-JAG is an emerging IETF draft specification ([draft-ietf-oauth-identity-assertion-authz-grant](https://datatracker.ietf.org/doc/draft-ietf-oauth-identity-assertion-authz-grant/)). Provider support is evolving rapidly. ## MCP Server Requirements ### Protected Resource Metadata (RFC 9728) MCP servers must expose `/.well-known/oauth-protected-resource`: ```json theme={null} { "resource": "https://mcp-server.internal.com", "authorization_servers": ["https://mcp-server.internal.com"], "scopes_supported": ["mcp:tools:read", "mcp:tools:call"] } ``` ### Token Endpoint MCP servers must implement a token endpoint accepting JWT Bearer Grants: ```typescript theme={null} // Token endpoint implementation if (url.pathname === '/oauth/token' && request.method === 'POST') { const body = await request.formData(); const grantType = body.get('grant_type'); // Only accept JWT Bearer Grant if (grantType !== 'urn:ietf:params:oauth:grant-type:jwt-bearer') { return Response.json({ error: 'unsupported_grant_type' }, { status: 400 }); } const assertion = body.get('assertion') as string; // Validate ID-JAG const validation = await validateIdJag(assertion, { trustedIssuers: [env.IDP_ISSUER], expectedAudience: env.MCP_RESOURCE_ID, idpJwksUrl: env.IDP_JWKS_URL, }); if (!validation.valid) { return Response.json({ error: 'invalid_grant' }, { status: 400 }); } // Issue access token const accessToken = await issueAccessToken({ sub: validation.claims.sub, aud: env.MCP_RESOURCE_ID, scope: validation.claims.scope, exp: Math.floor(Date.now() / 1000) + 3600, }, env.SIGNING_KEY); return Response.json({ access_token: accessToken, token_type: 'Bearer', expires_in: 3600, }); } ``` ### ID-JAG Validation | Check | Requirement | | ------------ | -------------------------------------------- | | Signature | Validate against IDP's JWKS | | `typ` header | Must be `oauth-id-jag+jwt` | | `iss` | Must match trusted IDP issuer | | `aud` | Must match MCP server's resource ID | | `client_id` | Must match expected Tool Hub client | | `exp` | Must be in future (60s clock skew tolerance) | ### Access Token Validation MCP servers validate incoming access tokens on protected endpoints: | Claim | Validation | | ----- | -------------------------------------- | | `iss` | Must match MCP server's token endpoint | | `aud` | Must match MCP server's resource ID | | `sub` | User identifier for authorization | | `exp` | Must be in future (60s clock skew) | ### Optional Claims | Claim | Purpose | | -------- | -------------------- | | `email` | User email address | | `groups` | Group memberships | | `roles` | Role assignments | | `scope` | Granted capabilities | ## Token Properties | Token Type | Issuer | Audience | Typical Lifetime | | ------------- | -------------- | ------------ | ---------------- | | User ID Token | Enterprise IDP | Frontend app | 1 hour | | ID-JAG | Enterprise IDP | MCP server | 5 minutes | | Access Token | MCP server | MCP server | 5-60 minutes | ## Hub Token Caching Cache key format: `token:{org_id}:{connector_id}:{user_id}` ```typescript theme={null} interface CachedToken { access_token: string; expires_at: number; refresh_token?: string; scopes: string[]; } ``` Cache TTL: `min(token_expiry - 300, 3600)` seconds. Tokens are stored in encrypted KV. Never stored in queryable databases. ## Transaction Tokens For MCP server internal service calls, use downscoped Transaction Tokens: | Property | Requirement | | ------------ | ---------------------------------- | | Audience | Bound to specific internal service | | Scope | Limited to specific operation | | Lifetime | Seconds to minutes | | User context | Embedded in signed claims | ## External MCP Servers For MCP servers operated by third parties (not your organization), use **OAuth 2.1 with PKCE**: * User explicitly consents via OAuth flow * Requires registration with the external MCP's auth server (not your enterprise IDP) * User identity is established through the external provider's OAuth, not your SSO This is the standard OAuth pattern—the user sees a consent screen and authorizes the Tool Hub to act on their behalf with that specific external service. ID-JAG is for **internal** MCP servers that trust your enterprise IDP. External MCP servers have their own identity systems and require explicit user consent. ## Specification Status | Component | Status | | -------------------------------------- | ------------------------------- | | RFC 8693 (Token Exchange) | Stable | | RFC 7523 (JWT Bearer Grant) | Stable | | RFC 8707 (Resource Indicators) | Stable | | RFC 9728 (Protected Resource Metadata) | Stable | | ID-JAG Draft | Draft, implementations emerging | | MCP Enterprise-Managed Authorization | Draft | ## References **OAuth/OIDC:** * [RFC 8693 - Token Exchange](https://datatracker.ietf.org/doc/html/rfc8693) * [RFC 7523 - JWT Bearer Grant](https://datatracker.ietf.org/doc/html/rfc7523) * [RFC 8707 - Resource Indicators](https://datatracker.ietf.org/doc/html/rfc8707) * [RFC 9728 - Protected Resource Metadata](https://datatracker.ietf.org/doc/html/rfc9728) * [Identity Assertion Authorization Grant (draft)](https://datatracker.ietf.org/doc/draft-ietf-oauth-identity-assertion-authz-grant/) **MCP:** * [MCP Authorization Spec](https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization) * [Enterprise-Managed Authorization Draft](https://github.com/modelcontextprotocol/ext-auth/blob/main/specification/draft/enterprise-managed-authorization.mdx) ## See Also Architecture and design rationale Identity delegation concepts Internal vs external MCP classification Token validation specifications # Overview Source: https://docs.usechar.com/reference/sdk/char Reference entry point for Char web components and host-shell APIs. # `@mcp-b/char` Framework-agnostic web components for embedding Char. ## Package entry points * `@mcp-b/char` (registers both elements) * `@mcp-b/char/web-component` * `@mcp-b/char/shell-component` * `@mcp-b/char/display-mode-policy` ## Generated reference * [Web Components Index](/reference/sdk/generated/char/index) * [`` API](/reference/sdk/generated/char/char-agent) * [`` API](/reference/sdk/generated/char/char-agent-shell) * [Custom Elements Manifest](/reference/sdk/generated/char/custom-elements-manifest) ## Related pages * [Agent attributes](/reference/agent-attributes) * [CSS variables](/reference/css-variables) # Overview Source: https://docs.usechar.com/reference/sdk/char-react Reference entry point for React wrappers over Char web components. # `@mcp-b/char-react` React convenience wrappers around the `@mcp-b/char` custom elements. ## Exports * `CharAgent` * `CharAgentShell` Both wrappers keep runtime behavior in the underlying custom elements and add React ergonomics: * ref forwarding * prop-to-attribute mapping * typed event callbacks * auth convenience via `setAuth(...)` ## Generated reference * [React API Index](/reference/sdk/generated/char-react/index) * [Display Mode Policy API Index](/reference/sdk/generated/char-policy/index) # isDisplayMode Source: https://docs.usechar.com/reference/sdk/generated/char-policy/functions/isDisplayMode Generated API reference for isDisplayMode. # Function: isDisplayMode() > **isDisplayMode**(`value`): `value is CharDisplayMode` Runtime guard for Char display mode values. ## Parameters ### value Raw value from host attributes, events, or external input. `string` | `null` | `undefined` ## Returns `value is CharDisplayMode` `true` when the value is one of `inline`, `fullscreen`, or `pip`. # isExpandedDisplayMode Source: https://docs.usechar.com/reference/sdk/generated/char-policy/functions/isExpandedDisplayMode Generated API reference for isExpandedDisplayMode. # Function: isExpandedDisplayMode() > **isExpandedDisplayMode**(`mode`): `boolean` Checks whether a display mode represents an expanded agent surface. Expanded modes are: * `inline` (panel-style) * `fullscreen` (viewport-filling) Collapsed mode is: * `pip` (launcher-only) ## Parameters ### mode Current display mode from host context. [`CharDisplayMode`](../type-aliases/CharDisplayMode) | `undefined` ## Returns `boolean` `true` for expanded modes and `false` for collapsed/undefined modes. # resolvePolicyDisplayMode Source: https://docs.usechar.com/reference/sdk/generated/char-policy/functions/resolvePolicyDisplayMode Generated API reference for resolvePolicyDisplayMode. # Function: resolvePolicyDisplayMode() > **resolvePolicyDisplayMode**(`options`): [`CharDisplayMode`](../type-aliases/CharDisplayMode) Resolves Char's standard host display-mode policy. Policy: * Closed state to `pip` * Open on desktop to `inline` * Open on narrow viewport to `fullscreen` The resolved mode is then constrained to `availableModes`. ## Parameters ### options Host orchestration inputs: `open`, `isNarrowViewport`, and optional `availableModes`. #### availableModes? readonly [`CharDisplayMode`](../type-aliases/CharDisplayMode)\[] #### isNarrowViewport `boolean` #### open `boolean` ## Returns [`CharDisplayMode`](../type-aliases/CharDisplayMode) Final mode that satisfies policy and availability constraints. # resolveSupportedDisplayMode Source: https://docs.usechar.com/reference/sdk/generated/char-policy/functions/resolveSupportedDisplayMode Generated API reference for resolveSupportedDisplayMode. # Function: resolveSupportedDisplayMode() > **resolveSupportedDisplayMode**(`preferred`, `availableModes?`): [`CharDisplayMode`](../type-aliases/CharDisplayMode) Resolves a preferred display mode against host-supported modes. An empty `availableModes` array means "no constraint" — the preferred mode is returned unconditionally. This is intentional: it allows callers to opt out of mode filtering without a separate code path. ## Parameters ### preferred [`CharDisplayMode`](../type-aliases/CharDisplayMode) Preferred mode requested by host policy. ### availableModes? readonly [`CharDisplayMode`](../type-aliases/CharDisplayMode)\[] = `DEFAULT_AVAILABLE_DISPLAY_MODES` Supported modes advertised by the host. ## Returns [`CharDisplayMode`](../type-aliases/CharDisplayMode) Preferred mode when supported, otherwise the first available mode. # Overview Source: https://docs.usechar.com/reference/sdk/generated/char-policy/index Generated TypeScript API reference for @mcp-b/char display-mode-policy. # Generated Display Mode Policy API Generated from TypeDoc using `packages/char/src/display-mode-policy.ts`. ## Constants * [DEFAULT\_AVAILABLE\_DISPLAY\_MODES](/reference/sdk/generated/char-policy/variables/DEFAULT_AVAILABLE_DISPLAY_MODES) ## Functions * [isDisplayMode](/reference/sdk/generated/char-policy/functions/isDisplayMode) * [isExpandedDisplayMode](/reference/sdk/generated/char-policy/functions/isExpandedDisplayMode) * [resolveSupportedDisplayMode](/reference/sdk/generated/char-policy/functions/resolveSupportedDisplayMode) * [resolvePolicyDisplayMode](/reference/sdk/generated/char-policy/functions/resolvePolicyDisplayMode) # DEFAULT AVAILABLE DISPLAY MODES Source: https://docs.usechar.com/reference/sdk/generated/char-policy/variables/DEFAULT_AVAILABLE_DISPLAY_MODES Generated API reference for DEFAULT AVAILABLE DISPLAY MODES. # Variable: DEFAULT\_AVAILABLE\_DISPLAY\_MODES > `const` **DEFAULT\_AVAILABLE\_DISPLAY\_MODES**: readonly \[`"inline"`, `"fullscreen"`, `"pip"`] Default display modes advertised by Char hosts. Order matters for fallback behavior. When a preferred mode is unavailable, the first available mode is selected. # Overview Source: https://docs.usechar.com/reference/sdk/generated/char-react/index Generated TypeScript API reference for @mcp-b/char-react. # Generated React API Generated from TypeDoc using `packages/char-react/src/index.ts`. ## Components * [CharAgent](/reference/sdk/generated/char-react/variables/CharAgent) * [CharAgentShell](/reference/sdk/generated/char-react/variables/CharAgentShell) ## Component props * [CharAgentProps](/reference/sdk/generated/char-react/interfaces/CharAgentProps) * [CharAgentShellProps](/reference/sdk/generated/char-react/interfaces/CharAgentShellProps) ## Shared types * [CharDisplayMode](/reference/sdk/generated/char-react/type-aliases/CharDisplayMode) * [CharPipPosition](/reference/sdk/generated/char-react/type-aliases/CharPipPosition) * [CharErrorDetail](/reference/sdk/generated/char-react/interfaces/CharErrorDetail) * [CharSizeChangedDetail](/reference/sdk/generated/char-react/interfaces/CharSizeChangedDetail) * [CharRequestDisplayModeDetail](/reference/sdk/generated/char-react/interfaces/CharRequestDisplayModeDetail) * [CharOpenLinkDetail](/reference/sdk/generated/char-react/interfaces/CharOpenLinkDetail) ## Element refs * [CharAgentElement](/reference/sdk/generated/char-react/interfaces/CharAgentElement) * [CharAgentShellElement](/reference/sdk/generated/char-react/interfaces/CharAgentShellElement) # CharAgentElement Source: https://docs.usechar.com/reference/sdk/generated/char-react/interfaces/CharAgentElement Generated API reference for CharAgentElement. # Interface: CharAgentElement `` custom element. Creates an iframe pointing to the SaaS app's /embed/ entrypoint and relays auth, styles, dark mode, display mode, and MCP messages via postMessage. Uses a unified `char-context` message with diffing (only changed fields are sent) instead of separate messages per concern. ## Extends * `HTMLElement` ## Properties ### apiBase? > `optional` **apiBase**: `string` React 19-friendly property form of `api-base`. Read once during `connectedCallback`. *** ### publishableKey? > `optional` **publishableKey**: `string` Publishable key for org identification. Set via `publishable-key` attribute or JS property. ## Methods ### attributeChangedCallback() > **attributeChangedCallback**(`name`, `_oldValue`, `newValue`): `void` Reacts to observed attribute updates after iframe readiness. Attributes that affect iframe boot configuration are intentionally ignored after mount (`enable-debug-tools`, `api-base`). #### Parameters ##### name `string` ##### \_oldValue `string` | `null` ##### newValue `string` | `null` #### Returns `void` *** ### connect() > **connect**(`options`): `boolean` Connect to the Char agent with authentication. The token is stored as a JavaScript property (not as a DOM attribute), preventing exposure to DOM inspection and session replay tools. #### Parameters ##### options `ConnectOptions` Authentication payload. #### Returns `boolean` `true` when the payload is accepted; `false` when validation fails. *** ### connectedCallback() > **connectedCallback**(): `void` Mount lifecycle: builds iframe runtime, starts proxying, and starts host observers. The existing runtime is reused when reconnecting during fast detach/reattach cycles. #### Returns `void` *** ### disconnect() > **disconnect**(): `boolean` Disconnect from the Char agent. Clears pending auth state and posts a disconnect message to the iframe. #### Returns `boolean` `true` when the disconnect message was sent; `false` when the iframe was not ready. *** ### disconnectedCallback() > **disconnectedCallback**(): `void` Unmount lifecycle: requests iframe teardown and releases all host resources. #### Returns `void` *** ### setAuth() > **setAuth**(`options`): `boolean` Convenience method for declarative wrappers. Applies auth when provided, otherwise clears auth. #### Parameters ##### options Auth payload to connect, or `null` to disconnect. `ConnectOptions` | `null` #### Returns `boolean` Result from `connect()` or `disconnect()`. *** ### setHostContext() > **setHostContext**(`hostContext`): `void` Update the host context sent to the iframe. Only changed fields are transmitted (diffing pattern). #### Parameters ##### hostContext `CharHostContext` Partial host context patch to merge and emit. #### Returns `void` # CharAgentProps Source: https://docs.usechar.com/reference/sdk/generated/char-react/interfaces/CharAgentProps Generated API reference for CharAgentProps. # Interface: CharAgentProps React props for the `CharAgent` wrapper. Auth contract: * `publishableKey` required * `idToken` optional ## Extends * `Omit`\<`HTMLAttributes`\<[`CharAgentElement`](CharAgentElement)>, `"children"` | `"onError"`> ## Properties ### apiBase? > `optional` **apiBase**: `string` *** ### displayMode? > `optional` **displayMode**: [`CharDisplayMode`](../type-aliases/CharDisplayMode) *** ### enableDebugTools? > `optional` **enableDebugTools**: `boolean` *** ### idToken? > `optional` **idToken**: `string` *** ### onCharClose()? > `optional` **onCharClose**: () => `void` #### Returns `void` *** ### onCharError()? > `optional` **onCharError**: (`detail`) => `void` #### Parameters ##### detail [`CharErrorDetail`](CharErrorDetail) #### Returns `void` *** ### onCharInitialized()? > `optional` **onCharInitialized**: () => `void` #### Returns `void` *** ### onCharOpenLink()? > `optional` **onCharOpenLink**: (`detail`) => `void` #### Parameters ##### detail [`CharOpenLinkDetail`](CharOpenLinkDetail) #### Returns `void` *** ### onCharRequestDisplayMode()? > `optional` **onCharRequestDisplayMode**: (`detail`) => `void` #### Parameters ##### detail [`CharRequestDisplayModeDetail`](CharRequestDisplayModeDetail) #### Returns `void` *** ### onCharSizeChanged()? > `optional` **onCharSizeChanged**: (`detail`) => `void` #### Parameters ##### detail [`CharSizeChangedDetail`](CharSizeChangedDetail) #### Returns `void` *** ### publishableKey > **publishableKey**: `string` # CharAgentShellElement Source: https://docs.usechar.com/reference/sdk/generated/char-react/interfaces/CharAgentShellElement Generated API reference for CharAgentShellElement. # Interface: CharAgentShellElement `` custom element. Opinionated host shell that composes a persistent inner `` and owns responsive mode policy plus host layout orchestration. This shell does not render chat controls, prompts, or composer UI. Visible assistant UI is always iframe-owned. Policy: * closed to `pip` * open desktop to `inline` * open narrow viewport to `fullscreen` ## Extends * `HTMLElement` ## Properties ### apiBase? > `optional` **apiBase**: `string` ## Accessors ### open #### Get Signature > **get** **open**(): `boolean` ##### Returns `boolean` #### Set Signature > **set** **open**(`value`): `void` ##### Parameters ###### value `boolean` ##### Returns `void` ## Methods ### attributeChangedCallback() > **attributeChangedCallback**(`name`, `_oldValue`, `newValue`): `void` #### Parameters ##### name `string` ##### \_oldValue `string` | `null` ##### newValue `string` | `null` #### Returns `void` *** ### connect() > **connect**(`options`): `boolean` Connects shell authentication and starts availability health checks. #### Parameters ##### options `ConnectOptions` Authentication payload (`publishableKey` + optional `idToken`). #### Returns `boolean` `true` when connect is accepted by the inner element. *** ### connectedCallback() > **connectedCallback**(): `void` #### Returns `void` *** ### disconnect() > **disconnect**(): `boolean` Clears authentication and disconnects the inner ``. #### Returns `boolean` `true` when disconnect dispatch succeeds. *** ### disconnectedCallback() > **disconnectedCallback**(): `void` #### Returns `void` *** ### setAuth() > **setAuth**(`options`): `boolean` Convenience method for declarative wrappers. Applies auth when provided, otherwise clears auth. #### Parameters ##### options Auth payload to connect, or `null` to disconnect. `ConnectOptions` | `null` #### Returns `boolean` Result from `connect()` or `disconnect()`. *** ### setHostContext() > **setHostContext**(`hostContext`): `void` Merges and forwards host context to the inner ``. Shell-owned fields (`displayMode`, `availableDisplayModes`) are ignored. #### Parameters ##### hostContext `CharHostContext` Partial host context patch to forward. #### Returns `void` *** ### setOpen() > **setOpen**(`open`): `void` Sets shell open state. #### Parameters ##### open `boolean` Next desired open state. #### Returns `void` *** ### toggleOpen() > **toggleOpen**(): `void` Toggles shell open state. #### Returns `void` # CharAgentShellProps Source: https://docs.usechar.com/reference/sdk/generated/char-react/interfaces/CharAgentShellProps Generated API reference for CharAgentShellProps. # Interface: CharAgentShellProps ## Extends * `Omit`\<`HTMLAttributes`\<[`CharAgentShellElement`](CharAgentShellElement)>, `"children"` | `"onError"`> ## Properties ### apiBase? > `optional` **apiBase**: `string` *** ### defaultOpen? > `optional` **defaultOpen**: `boolean` *** ### enableDebugTools? > `optional` **enableDebugTools**: `boolean` *** ### idToken? > `optional` **idToken**: `string` *** ### onCharClose()? > `optional` **onCharClose**: () => `void` #### Returns `void` *** ### onCharError()? > `optional` **onCharError**: (`detail`) => `void` #### Parameters ##### detail [`CharErrorDetail`](CharErrorDetail) #### Returns `void` *** ### onCharInitialized()? > `optional` **onCharInitialized**: () => `void` #### Returns `void` *** ### onCharOpenLink()? > `optional` **onCharOpenLink**: (`detail`) => `void` #### Parameters ##### detail [`CharOpenLinkDetail`](CharOpenLinkDetail) #### Returns `void` *** ### onCharRequestDisplayMode()? > `optional` **onCharRequestDisplayMode**: (`detail`) => `void` #### Parameters ##### detail [`CharRequestDisplayModeDetail`](CharRequestDisplayModeDetail) #### Returns `void` *** ### onCharSizeChanged()? > `optional` **onCharSizeChanged**: (`detail`) => `void` #### Parameters ##### detail [`CharSizeChangedDetail`](CharSizeChangedDetail) #### Returns `void` *** ### onOpenChange()? > `optional` **onOpenChange**: (`open`) => `void` #### Parameters ##### open `boolean` #### Returns `void` *** ### open? > `optional` **open**: `boolean` *** ### pipPosition? > `optional` **pipPosition**: [`CharPipPosition`](../type-aliases/CharPipPosition) *** ### publishableKey > **publishableKey**: `string` # CharErrorDetail Source: https://docs.usechar.com/reference/sdk/generated/char-react/interfaces/CharErrorDetail Generated API reference for CharErrorDetail. # Interface: CharErrorDetail Payload for the `char-error` event. ## Properties ### code > **code**: `string` *** ### message? > `optional` **message**: `string` # CharOpenLinkDetail Source: https://docs.usechar.com/reference/sdk/generated/char-react/interfaces/CharOpenLinkDetail Generated API reference for CharOpenLinkDetail. # Interface: CharOpenLinkDetail Payload for the `char-open-link` event. ## Properties ### ok > **ok**: `boolean` *** ### requestId > **requestId**: `string` *** ### url > **url**: `string` # CharRequestDisplayModeDetail Source: https://docs.usechar.com/reference/sdk/generated/char-react/interfaces/CharRequestDisplayModeDetail Generated API reference for CharRequestDisplayModeDetail. # Interface: CharRequestDisplayModeDetail Payload for the `char-request-display-mode` event. ## Properties ### mode? > `optional` **mode**: [`CharDisplayMode`](../type-aliases/CharDisplayMode) # CharSizeChangedDetail Source: https://docs.usechar.com/reference/sdk/generated/char-react/interfaces/CharSizeChangedDetail Generated API reference for CharSizeChangedDetail. # Interface: CharSizeChangedDetail Payload for the `char-size-changed` event. ## Properties ### displayMode? > `optional` **displayMode**: [`CharDisplayMode`](../type-aliases/CharDisplayMode) *** ### height? > `optional` **height**: `number` *** ### width? > `optional` **width**: `number` # CharDisplayMode Source: https://docs.usechar.com/reference/sdk/generated/char-react/type-aliases/CharDisplayMode Generated API reference for CharDisplayMode. # Type Alias: CharDisplayMode > **CharDisplayMode** = `"inline"` | `"fullscreen"` | `"pip"` Allowed display modes for the host shell and embedded runtime. # CharPipPosition Source: https://docs.usechar.com/reference/sdk/generated/char-react/type-aliases/CharPipPosition Generated API reference for CharPipPosition. # Type Alias: CharPipPosition > **CharPipPosition** = `"bottom-center"` | `"bottom-right"` | `"bottom-left"` Allowed positions for the collapsed PIP composer surface. # CharAgent Source: https://docs.usechar.com/reference/sdk/generated/char-react/variables/CharAgent Generated API reference for CharAgent. # Variable: CharAgent > `const` **CharAgent**: `ForwardRefExoticComponent`\<[`CharAgentProps`](../interfaces/CharAgentProps) & `RefAttributes`\<[`CharAgentElement`](../interfaces/CharAgentElement)>> # CharAgentShell Source: https://docs.usechar.com/reference/sdk/generated/char-react/variables/CharAgentShell Generated API reference for CharAgentShell. # Variable: CharAgentShell > `const` **CharAgentShell**: `ForwardRefExoticComponent`\<[`CharAgentShellProps`](../interfaces/CharAgentShellProps) & `RefAttributes`\<[`CharAgentShellElement`](../interfaces/CharAgentShellElement)>> # Source: https://docs.usechar.com/reference/sdk/generated/char/char-agent Custom element API reference for . # `` `` custom element. Creates an iframe pointing to the SaaS app's /embed/ entrypoint and relays auth, styles, dark mode, display mode, and MCP messages via postMessage. Uses a unified `char-context` message with diffing (only changed fields are sent) instead of separate messages per concern. ## Registration and import ```ts theme={null} import '@mcp-b/char/web-component' ``` ```ts theme={null} import { registerChar } from '@mcp-b/char/web-component' registerChar() ``` ## Auth and connection helpers ```ts theme={null} const agent = document.querySelector('char-agent') agent?.setAuth({ publishableKey, idToken }) // convenience wrapper agent?.connect({ publishableKey, idToken }) // explicit connect agent?.setHostContext({ displayMode: 'inline' }) agent?.disconnect() ``` * [`setAuth`](#method-setauth) * [`connect`](#method-connect) * [`disconnect`](#method-disconnect) * [`setHostContext`](#method-sethostcontext) ## Attributes | Attribute | Type | Default | Description | | -------------------- | ----------------------------------- | ------------------------ | ------------------------------------------------ | | `api-base` | `string` | `https://app.usechar.ai` | Overrides the Char API base URL. | | `display-mode` | `"inline" \| "fullscreen" \| "pip"` | `pip` | Requested display mode for the embedded surface. | | `enable-debug-tools` | `boolean` | `false` | Enables iframe debug tools when supported. | | `publishable-key` | `string` | n/a | Publishable key used to resolve org context. | ## Properties | Property | Type | Description | | ---------------- | --------------------- | ------------------------------------------------------------------------------------------- | | `apiBase` | `string \| undefined` | React-friendly property form of `api-base`. | | `publishableKey` | `string \| undefined` | Publishable key for org identification. Set via `publishable-key` attribute or JS property. | ## Methods | Method | Returns | Summary | | -------------------------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [`setAuth()`](#method-setauth) | `void` | Convenience method for declarative wrappers. Applies auth when provided, otherwise clears auth. | | [`connect()`](#method-connect) | `void` | Connect to the Char agent with authentication. The token is stored as a JavaScript property (not as a DOM attribute), preventing exposure to DOM inspection and session replay tools. | | [`disconnect()`](#method-disconnect) | `void` | Disconnect from the Char agent. Clears pending auth state and posts a disconnect message to the iframe. | | [`setHostContext()`](#method-sethostcontext) | `void` | Update the host context sent to the iframe. Only changed fields are transmitted (diffing pattern). | ### `setAuth(options: ConnectOptions | null): void` Convenience method for declarative wrappers. Applies auth when provided, otherwise clears auth. Parameters: * `options` (`ConnectOptions | null`): Auth payload to connect, or `null` to disconnect. Returns: `void` ### `connect(options: ConnectOptions): void` Connect to the Char agent with authentication. The token is stored as a JavaScript property (not as a DOM attribute), preventing exposure to DOM inspection and session replay tools. Parameters: * `options` (`ConnectOptions`): Authentication payload. Returns: `void` ### `disconnect(): void` Disconnect from the Char agent. Clears pending auth state and posts a disconnect message to the iframe. Returns: `void` ### `setHostContext(hostContext: CharHostContext): void` Update the host context sent to the iframe. Only changed fields are transmitted (diffing pattern). Parameters: * `hostContext` (`CharHostContext`): Partial host context patch to merge and emit. Returns: `void` ## Events | Event | Detail | Description | | --------------------------- | ------------------------------ | ---------------------------------------------------- | | `char-initialized` | `void` | Emitted when the embedded runtime has initialized. | | `char-close` | `void` | Emitted when the embedded UI asks the host to close. | | `char-error` | `CharErrorDetail` | Emitted on host/runtime bridge errors. | | `char-size-changed` | `CharSizeChangedDetail` | Reports embedded content sizing updates. | | `char-request-display-mode` | `CharRequestDisplayModeDetail` | Requests a host-side display mode change. | | `char-open-link` | `CharOpenLinkDetail` | Requests the host to open an external URL. | ## Slots *None.* ## CSS Custom Properties Supports the full Char design-token surface documented in [CSS Variables Reference](/reference/css-variables). ## CSS Parts *None.* # Source: https://docs.usechar.com/reference/sdk/generated/char/char-agent-shell Custom element API reference for . # `` `` custom element. Opinionated host shell that composes a persistent inner `` and owns responsive mode policy plus host layout orchestration. This shell does not render chat controls, prompts, or composer UI. Visible assistant UI is always iframe-owned. Policy: * closed to `pip` * open desktop to `inline` * open narrow viewport to `fullscreen` ## Registration and import ```ts theme={null} import '@mcp-b/char/shell-component' ``` ```ts theme={null} import { registerCharShell } from '@mcp-b/char/shell-component' registerCharShell() ``` ## Auth, open-state, and shell helpers ```ts theme={null} const shell = document.querySelector('char-agent-shell') shell?.setAuth({ publishableKey, idToken }) // convenience wrapper shell?.setOpen(true) // controlled open state shell?.setHostContext({ locale: 'en-US' }) shell?.toggleOpen() ``` * [`setAuth`](#method-setauth) * [`connect`](#method-connect) * [`disconnect`](#method-disconnect) * [`setHostContext`](#method-sethostcontext) * [`setOpen`](#method-setopen) * [`toggleOpen`](#method-toggleopen) ## Attributes | Attribute | Type | Default | Description | | -------------------- | --------- | ------------------------ | --------------------------------------------------- | | `api-base` | `string` | `https://app.usechar.ai` | Forwarded API base override for the inner element. | | `enable-debug-tools` | `boolean` | `false` | Forwarded debug-tool toggle for the iframe runtime. | | `open` | `boolean` | `false` | Open state for policy-resolved layout behavior. | ## Properties | Property | Type | Description | | --------- | --------------------- | -------------------------------------------------- | | `apiBase` | `string \| undefined` | React-friendly property form of `api-base`. | | `open` | `boolean` | Current open/closed state for shell layout policy. | ## Methods | Method | Returns | Summary | | -------------------------------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------- | | [`setAuth()`](#method-setauth) | `void` | Convenience method for declarative wrappers. Applies auth when provided, otherwise clears auth. | | [`connect()`](#method-connect) | `void` | Connects shell authentication and starts availability health checks. | | [`disconnect()`](#method-disconnect) | `void` | Clears authentication and disconnects the inner ``. | | [`setHostContext()`](#method-sethostcontext) | `void` | Merges and forwards host context to the inner ``. Shell-owned fields (`displayMode`, `availableDisplayModes`) are ignored. | | [`setOpen()`](#method-setopen) | `void` | Sets shell open state. | | [`toggleOpen()`](#method-toggleopen) | `void` | Toggles shell open state. | ### `setAuth(options: ConnectOptions | null): void` Convenience method for declarative wrappers. Applies auth when provided, otherwise clears auth. Parameters: * `options` (`ConnectOptions | null`): Auth payload to connect, or `null` to disconnect. Returns: `void` ### `connect(options: ConnectOptions): void` Connects shell authentication and starts availability health checks. Parameters: * `options` (`ConnectOptions`): Authentication payload (`publishableKey` + optional `idToken`). Returns: `void` ### `disconnect(): void` Clears authentication and disconnects the inner ``. Returns: `void` ### `setHostContext(hostContext: CharHostContext): void` Merges and forwards host context to the inner ``. Shell-owned fields (`displayMode`, `availableDisplayModes`) are ignored. Parameters: * `hostContext` (`CharHostContext`): Partial host context patch to forward. Returns: `void` ### `setOpen(open: boolean): void` Sets shell open state. Parameters: * `open` (`boolean`): Next desired open state. Returns: `void` ### `toggleOpen(): void` Toggles shell open state. Returns: `void` ## Events | Event | Detail | Description | | --------------------------- | ------------------------------ | ------------------------------------------------- | | `char-shell-open-change` | `{ open: boolean }` | Emitted when the shell open state changes. | | `char-initialized` | `void` | Forwarded from the inner `` runtime. | | `char-close` | `void` | Forwarded when embedded UI requests close. | | `char-error` | `CharErrorDetail` | Forwarded bridge/runtime errors. | | `char-size-changed` | `CharSizeChangedDetail` | Forwarded embedded content size updates. | | `char-request-display-mode` | `CharRequestDisplayModeDetail` | Forwarded mode-change requests from the iframe. | | `char-open-link` | `CharOpenLinkDetail` | Bubbled link-open request from the inner element. | ## Slots *None.* ## CSS Custom Properties Forwards styling to the inner ``. See [CSS Variables Reference](/reference/css-variables). ## CSS Parts *None.* # Custom Elements Manifest Source: https://docs.usechar.com/reference/sdk/generated/char/custom-elements-manifest Download and inspect the generated custom-elements.json for @mcp-b/char. # Custom Elements Manifest This file is generated from: * `packages/char/src/web-component.tsx` * `packages/char/src/shell-component.ts` Download: [`custom-elements.json`](./custom-elements.json) Use this file for external tooling that consumes the Custom Elements Manifest specification. # Overview Source: https://docs.usechar.com/reference/sdk/generated/char/index Generated API reference for Char custom elements. # Generated Web Components These pages are generated from `packages/char/dist/custom-elements.json`. * [``](/reference/sdk/generated/char/char-agent) * [``](/reference/sdk/generated/char/char-agent-shell) * [Custom Elements Manifest](/reference/sdk/generated/char/custom-elements-manifest) # Overview Source: https://docs.usechar.com/reference/sdk/index Reference docs for the @mcp-b/char and @mcp-b/char-react SDKs. # SDK Reference API reference for Char embedding SDKs. * [`@mcp-b/char`](/reference/sdk/char) * [`@mcp-b/char-react`](/reference/sdk/char-react) * [Generated Web Components](/reference/sdk/generated/char/index) * [Generated React API](/reference/sdk/generated/char-react/index) * [Generated Display Mode Policy API](/reference/sdk/generated/char-policy/index) ## Source-of-truth split * Web component API (``, ``): generated from Custom Elements Manifest. * TypeScript package API (`@mcp-b/char-react`, display-mode helpers): generated from TypeDoc and API Extractor. # Security Reference Source: https://docs.usechar.com/reference/security Technical security controls for publishable keys, token validation, and runtime isolation This page describes implemented security controls in the current auth model. ## Ticket exchange security (`/api/auth/pk-ticket`) 1. Validate `publishable_key` format 2. Hash key and look up active key record 3. Resolve org context from key 4. Enforce per-key `allowed_origins` (required, no org-level fallback) 5. If `id_token` provided: validate signature/issuer/audience using org IDP config 6. Issue short-lived ticket with org + user identity ## Publishable key controls | Control | Behavior | | ------------------- | ------------------------------------------------------------------ | | Stored format | SHA-256 hash (`key_hash`), never plaintext | | Display format | Prefix only (`key_prefix`) in UI | | Revocation | Immediate via `revoked_at` | | Origin restrictions | Per-key `allowed_origins` (required on every key, no org fallback) | | Usage tracking | Best-effort `last_used_at` updates | ## Token validation controls | Claim | Validation | | ----- | ----------------------------------------- | | `iss` | Must match configured issuer/domain | | `sub` | Required for identity mapping | | `aud` | Must match configured allowed audience(s) | | `exp` | Must be valid at verification time | Algorithms are limited to asymmetric signatures (no `none`, no shared-secret algorithm bypasses). ## Runtime isolation | Component | Isolation boundary | | ---------------- | --------------------------------------------- | | ThreadManager DO | User/org scoped metadata and connection state | | ThreadAgent DO | Thread-scoped execution state | | Tool routing | Routed to connection that registered the tool | ## Recommended operational practices * Scope keys by environment (dev/staging/prod) * Restrict key origins tightly * Revoke keys during incident response * Keep IDP issuer/audience config in sync with auth provider * Monitor auth failures (`INVALID_KEY`, origin failures, token failures) # Skill Schema Source: https://docs.usechar.com/reference/skill-schema SKILL.md format specification with YAML frontmatter fields, name validation rules, and API endpoints Skills are Markdown files with YAML frontmatter. This page documents the complete schema. ## File Structure A skill file follows this structure: ``` --- --- ``` The frontmatter is delimited by `---` on its own line. Everything after the closing delimiter is the body. ## Storage Model in Char Char uses two skill sources: 1. **Organization skills (DB-backed)**\ Stored per organization and managed through the Skills UI and API. 2. **Built-in global skills (code-owned)**\ Shared across all organizations and exposed through fixed `location` values. Both sources use the same `SKILL.md` content format documented on this page. Current built-in global skill: * `skill-creator-webmcp` (`location`: `builtin://skill-creator-webmcp`) ## Frontmatter Fields ### Required Skill identifier. * 1–64 characters * Lowercase letters, numbers, hyphens * No leading hyphen * No trailing hyphen * No consecutive hyphens * Unicode allowed (NFKC normalized) When the skill should be used. * 1–1024 characters ### Optional License identifier (e.g., `MIT`, `Apache-2.0`). Runtime requirements or environment notes. * Maximum 500 characters Space-delimited list of tool names. ```yaml theme={null} allowed-tools: "search_orders process_refund" ``` Arbitrary key-value pairs. ```yaml theme={null} metadata: author: support-team version: "1.0" ``` ## Name Validation | Input | Valid | Reason | | ------------------- | ----- | ------------------- | | `customer-support` | Yes | | | `order-lookup` | Yes | | | `销售分析` | Yes | Unicode allowed | | `Customer-Support` | No | Uppercase | | `-customer-support` | No | Leading hyphen | | `customer-support-` | No | Trailing hyphen | | `customer--support` | No | Consecutive hyphens | ## Body The body is Markdown. No schema constraints apply. Content appears verbatim in the agent's context when the skill is activated. ## Example ```markdown theme={null} --- name: refund-processor description: Handle refund requests for orders within the return window license: MIT compatibility: Requires order_management tools metadata: owner: billing version: "1.2" --- # Refund Processor Process refund requests according to policy. ## Eligibility - Order within 30-day window - Item unused and in original packaging - Customer provides order ID ## Process 1. Verify eligibility 2. Confirm refund amount 3. Process refund 4. Send confirmation ## Escalation Transfer to human support for: - Requests outside window - Amounts over $500 - Disputed charges ``` ## API ### List Skills ``` POST /organization-skills/list ``` Returns skill metadata (name, description, id). Does not include body. ### Get Skill ``` GET /organization-skills/{id} ``` Returns full skill including body. ### Create Skill ``` POST /organization-skills ``` Request body: `{ "content": "" }` ### Update Skill ``` PATCH /organization-skills/{id} ``` Accepts patch operations. ### Delete Skill ``` DELETE /organization-skills/{id} ``` Archives the skill. Does not permanently delete. ## Validation Errors | Code | Message | Cause | | ------------------ | -------------------------------------- | ---------------------------- | | `VALIDATION_ERROR` | `name is required` | Missing name field | | `VALIDATION_ERROR` | `name must be lowercase` | Uppercase characters in name | | `VALIDATION_ERROR` | `invalid name format` | Hyphenation rules violated | | `VALIDATION_ERROR` | `description is required` | Missing description field | | `VALIDATION_ERROR` | `description exceeds 1024 characters` | Description too long | | `VALIDATION_ERROR` | `compatibility exceeds 500 characters` | Compatibility too long | ## Token Estimates | Content | Typical Range | | ------------- | --------------- | | Metadata only | 50–100 tokens | | Full skill | 500–5000 tokens | # Token Issuer Metadata Source: https://docs.usechar.com/reference/token-issuer-metadata A CIMD extension for passive token validation that eliminates manual SSO configuration This is a thought experiment: what if embedded services could just be plopped in your frontend with zero configuration while still being secure? This isn't something I plan to implement, but I thought it was interesting and worth sharing. What if embedded services never needed manual SSO configuration? No dashboard. No shared secrets. Customers host a metadata file declaring their identity provider, and any service that implements the spec can validate their users' tokens automatically. This document explains the problem, proposes a solution, and sketches a specification. *** ## Part 1: Why Self-Hosted Bootstrap Matters ### What Char Does Today Char validates tokens from your identity provider. It doesn't initiate authentication. It receives tokens your app already obtained and verifies them using public keys. This is different from "Login with Google" flows. When Slack implements Google login, Slack acts as an OAuth *Client*. Slack needs its own Client ID registered with Google. Every vendor needs their own registration. Char acts as a **Passive Validator**. We don't initiate auth. We don't need our own Client ID with your IDP. We validate tokens that were already issued to *your* app: the tokens your users already have. ```mermaid theme={null} sequenceDiagram participant User participant YourApp as Your App participant IDP as Your IDP participant Char Note over YourApp,IDP: Existing relationship (client_id: "abc123") User->>YourApp: Visit app YourApp->>IDP: Redirect to login IDP->>User: Authenticate IDP->>YourApp: Token (aud: "abc123") YourApp->>Char: Here's the token Note over Char: Char has no relationship with IDP Char->>Char: Which IDP should I validate against? Char->>IDP: Fetch public JWKS Char->>Char: Verify signature, check claims Char->>YourApp: ✓ Valid ``` The question Char needs answered: **which IDP issued this token?** Once we know that, validation is standard OIDC: fetch the IDP's public keys, verify the signature, check the issuer and audience claims. ### Manual Configuration Today You tell us which IDP to validate against by configuring it in our dashboard: 1. Log into Char dashboard 2. Navigate to Settings → Integration 3. Select your IDP type (Okta, Google, Auth0...) 4. Copy your Client ID from your IDP 5. Paste it into our form 6. Copy your IDP domain 7. Paste it into our form 8. Add your application's domain to our allowlist 9. Click Save This works. But it's friction. Every form field is a potential mistake. Every context-switch between your IDP console and our dashboard is a chance to abandon the integration entirely. The deeper problem: this configuration is *redundant*. You already know your IDP configuration (it's in your IDP). You already control your domain (that's where you're embedding the agent). We're asking you to re-state facts that already exist elsewhere, in a format we invented, in a system you have to learn. ```mermaid theme={null} graph LR subgraph "Today" A1[Your App] --> B1[Char Dashboard] B1 --> C1[Manual Config] C1 --> D1[Char validates tokens] end ``` ```mermaid theme={null} graph LR subgraph "With CIMD" A2[Your App] --> B2[Hosts CIMD file] B2 --> C2[Char auto-discovers] C2 --> D2[Char validates tokens] end ``` The dashboard step disappears. Configuration moves from our system to yours. ### Declarative Configuration What if you declared your IDP configuration in a file you control? You host a JSON document at a well-known URL. When a request arrives from your domain, we fetch the file, learn which IDP to validate against, and proceed. No dashboard. No copy-paste. The configuration lives in your infrastructure, under your version control. This isn't a new idea. **Client ID Metadata Documents (CIMD)** emerged from a similar frustration in the OAuth world. The MCP ecosystem faced a scaling problem: AI clients connecting to thousands of servers, each requiring registration. CIMD inverts the model. Instead of clients registering with servers, clients publish their own metadata at a URL they control. The URL *is* the identity. Domain ownership becomes the trust anchor. The MCP spec adopted CIMD in November 2025. Auth0, Stytch, WorkOS, and others have published guidance and product support for CIMD in the MCP context (availability varies by provider). **This isn't OIDC Federation.** OIDC Federation solves a different problem: "Should this IDP issue tokens to an unknown Relying Party?" That requires trust chains, signed Entity Statements, and complex machinery, because the IDP is making a consequential decision. We're solving a narrower problem: "Which IDP should I validate this token against?" We don't ask the IDP for permission. We just fetch its public keys (which are public) and verify signatures. Domain ownership is sufficient trust for that question. ### CIMD Extension Extend CIMD with IDP configuration. A customer hosts a file at `/.well-known/oauth-client` that includes: ```json theme={null} { "client_id": "https://crm.acme.com/.well-known/oauth-client", "client_name": "Acme CRM", "token_issuer": { "issuer": "https://acme.okta.com/oauth2/default", "expected_audience": "0oa..." } } ``` When Char receives a request from `crm.acme.com`, we fetch this file, learn that tokens should be validated against the declared issuer with that expected audience, and proceed. The customer declared their truth in a file they control. We read it. No dashboard required. This isn't just about reducing clicks. It's about putting configuration where it belongs: with the customer, in their infrastructure, under their version control. ### The Trust Model The elegance of this approach is that it doesn't require new trust mechanisms. It composes existing ones: **Domain ownership** is already how the web establishes identity. If you can host files at `crm.acme.com`, you control that domain. HTTPS and DNS provide that guarantee. CIMD simply uses it. **IDP tokens** are already how enterprises establish user identity. The customer's IDP signs tokens with keys only it possesses. We validate signatures against published public keys. This is [federated authentication](/explanation/federated-auth), and we already do it. **Audience claims** already prevent token confusion. A token for one application can't be used for another. We already validate this. CIMD-based discovery just connects these existing mechanisms. The customer's domain proves they're legitimate. Their CIMD declares their IDP. Their IDP's tokens prove user identity. No new cryptography. No new trust assumptions. Just composition. ### What This Enables **Instant trials.** Embed the agent, host a CIMD, it works. No account required. Conversion happens when customers hit limits, not before they've seen why the product matters. **Configuration-as-code.** The CIMD is a file in the repository. It goes through code review. It deploys through CI/CD. Staging and production differ by environment, not by dashboard. **Self-service onboarding.** If you can host a valid CIMD, you're onboarded. No waiting for us to approve anything. **Single source of truth.** Configuration lives in one place: yours. We don't have a stale copy that disagrees with your IDP. ### Broader Applicability This pattern isn't specific to Char. Any embedded SaaS that needs to verify user identity faces the same problem. Today, services like Intercom and Zendesk use HMAC shared secrets: the customer generates a secret, passes user data server-side, and the service validates the signature. This works, but it requires server-side integration and secret management. Every vendor has their own approach. Token Issuer Metadata eliminates this. If your app already authenticates users via OIDC (most do), you're already issuing tokens. Third-party services embedded in your app can validate those tokens directly. One CIMD file, hosted once, works for any service that implements the spec. The network effect: once a customer hosts a CIMD with `token_issuer`, every third-party validator benefits. The first vendor to push adoption creates value for the whole ecosystem. ### The Tradeoffs This model has genuine limitations worth acknowledging: **GitOps vs. ClickOps.** This model shifts configuration from "ClickOps" (admins pasting values into dashboards) to "GitOps" (engineers committing config to code). For engineering-led organizations, this is a feature: version control, code review, reproducible environments. For non-technical teams who can't easily host a JSON file, it's a barrier. The hybrid approach (supporting both CIMD and dashboard) bridges this gap. **No pre-flight validation.** With dashboard configuration, customers can click "Test Connection" before users arrive. With CIMD discovery, the first real user is the test. Errors surface in production. A `char cimd validate` CLI tool or web-based validator could help customers catch errors before deploying. **Bootstrap, not runtime.** CIMD fetch happens once, when we first see requests from a new origin. After that, the configuration lives in our infrastructure. This means CIMD availability isn't a runtime concern: if your app loads, we can reach your CIMD. (Char is embedded in your page. If your CDN is down, the page doesn't load, and we never receive a request.) What *does* matter is config drift. If you rotate your Okta client ID but forget to update the CIMD file, tokens will fail validation. **Domain compromise scope.** If an attacker gains control of a customer's domain, they can modify the CIMD. This is true of any domain-based trust model (it's the same threat model as HTTPS itself), but worth noting explicitly. **Standardization.** The `token_issuer` extension doesn't exist as a standard yet. This document proposes one. Adoption depends on whether other vendors see value in a shared approach. *** ## Part 2: Technical Specification The following sketches what a specification for CIMD-based IDP discovery might look like. ### Extended CIMD Document A standard CIMD document contains OAuth client metadata. We extend it with identity provider configuration: ```json theme={null} { "client_id": "https://crm.acme.com/.well-known/oauth-client", "client_name": "Acme CRM", "client_uri": "https://crm.acme.com", "redirect_uris": ["https://crm.acme.com/callback"], "token_issuer": { "issuer": "https://accounts.google.com", "expected_audience": "123456789.apps.googleusercontent.com" } } ``` The `token_issuer` object declares how to validate tokens from users of this application. Validators fetch `{issuer}/.well-known/openid-configuration` to discover the JWKS endpoint, then verify that the token's `aud` claim matches `expected_audience`. **On standardization:** The `token_issuer` field is a proposed extension to CIMD. While CIMD itself is an IETF draft with growing adoption, there is no standard for declaring IDP configuration inside a client metadata document. This proposal suggests one. Char is an early implementer, but the pattern is designed to be vendor-neutral and adoptable by any service that validates tokens passively. ### Token Issuer Fields | Field | Required | Description | | ------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `issuer` | Yes | Full issuer URL. Must match the token's `iss` claim. JWKS discovered via `{issuer}/.well-known/openid-configuration`. | | `expected_audience` | Yes | Expected value of the token's `aud` claim. This is typically the OAuth client ID of the customer's application. | | `type` | No | Optional hint: `google`, `okta`, `azure`, `auth0`. Used for IDP-specific validation quirks (e.g., Google accepts both `https://accounts.google.com` and `accounts.google.com` as issuer). | **Examples by IDP:** ```json theme={null} // Google "token_issuer": { "issuer": "https://accounts.google.com", "expected_audience": "123...apps.googleusercontent.com", "type": "google" // optional: handles dual-issuer quirk } // Okta (note: issuer includes the authorization server path) "token_issuer": { "issuer": "https://acme.okta.com/oauth2/default", "expected_audience": "0oa..." } // Azure AD (note: issuer includes tenant ID) "token_issuer": { "issuer": "https://login.microsoftonline.com/{tenant-id}/v2.0", "expected_audience": "..." } // Auth0 "token_issuer": { "issuer": "https://acme.auth0.com/", "expected_audience": "..." } // Any OIDC-compliant provider "token_issuer": { "issuer": "https://auth.acme.com", "expected_audience": "..." } ``` ### Optional Admin Configuration To enable dashboard access for auto-provisioned organizations: ```json theme={null} { "token_issuer": { ... }, "admin_contact": { "email": "admin@acme.com" } } ``` The first user whose token contains this email becomes the organization owner. ### Discovery Flow ```mermaid theme={null} sequenceDiagram participant User participant App as Customer App participant Char participant CIMD as Customer CIMD participant IDP as Customer IDP User->>App: Visit application User->>IDP: Authenticate IDP-->>User: JWT token User->>Char: Request with token + Origin header alt Origin unknown Char->>CIMD: GET {origin}/.well-known/oauth-client CIMD-->>Char: IDP configuration Char->>Char: Create organization record end Char->>IDP: Fetch JWKS Char->>Char: Validate token signature Char->>Char: Validate issuer, audience, expiry Char-->>User: Authenticated session ``` ### Auto-Provisioning Behavior When Char receives a request from an unknown origin with a valid-looking token: **Discovery:** Fetch `{origin}/.well-known/oauth-client`. If not found or invalid, reject the request (fall back to requiring dashboard configuration). **Validation:** Parse the `token_issuer` extension. Validate the token against the declared IDP: fetch JWKS, verify signature, check issuer and audience claims. **Provisioning:** If validation succeeds, create an organization record with: * The origin as the primary allowed domain * IDP configuration from the CIMD * Free tier defaults * The authenticated user as the first end-user **Persistence:** The organization record lives in Char's infrastructure (Durable Objects). Subsequent requests validate against the stored config. We don't re-fetch CIMD on every auth. CIMD is the bootstrap mechanism, not a runtime dependency. Periodic re-fetching could detect config changes, but token validation doesn't depend on it. ### Dashboard Access Flow For customers to access the dashboard after auto-provisioning: ```mermaid theme={null} sequenceDiagram participant Admin participant Dashboard as Char Dashboard participant IDP as Customer IDP participant Char Admin->>Dashboard: Click "Sign In" Dashboard->>Admin: Select IDP or enter domain Admin->>IDP: Authenticate IDP-->>Admin: JWT token Admin->>Dashboard: Token Dashboard->>Char: Lookup org by IDP tenant/domain Char-->>Dashboard: Organization found Dashboard-->>Admin: Logged in as org admin ``` Matching logic: * For Google: match by `hd` (hosted domain) claim * For Okta/Auth0: match by issuer URL * For Azure: match by `tid` (tenant ID) claim * Fallback: match by email domain ### Security Considerations #### Core Validations **CIMD URL validation:** The `client_id` field in the CIMD must exactly match the URL it was fetched from. This prevents an attacker from hosting a CIMD that claims to be a different domain. **HTTPS required:** CIMD must be served over HTTPS. HTTP URLs are rejected. **Issuer validation:** The token's `iss` claim must match the expected issuer for the declared IDP type. A CIMD declaring `type: google` cannot be used to validate tokens from Okta. **Audience validation:** The token's `aud` claim must match the `expected_audience` declared in the CIMD. This prevents tokens intended for other applications from being accepted. **Origin binding (preventing token replay):** A natural concern: "If you accept tokens meant for other apps, can't I steal a token from App A and use it on App B?" No, because the CIMD is fetched from the request's origin: 1. Attacker steals token from `app-a.com` (where `aud: "client-id-A"`) 2. Attacker sends token to Char from `app-b.com` 3. Char fetches CIMD from `app-b.com` 4. CIMD declares `expected_audience: "client-id-B"` 5. Token's `aud` doesn't match → **Rejected** The token is bound to the origin that declares it. A token valid for one domain cannot be used from another domain, even if both use the same IDP. **On Origin headers:** Origin can be spoofed by non-browser clients. Origin binding is a routing hint, not a cryptographic proof. The actual security boundary is audience validation: the token's `aud` claim must match the CIMD's declared `expected_audience`. If an attacker could forge a valid token with the correct audience, they wouldn't need to spoof Origin in the first place. #### SSRF Hardening (Normative) Fetching customer-hosted metadata creates SSRF risk. Implementations MUST: * **Reject non-HTTPS URLs.** No `http://`, `file://`, or other schemes. * **Block private/loopback addresses.** Resolve DNS before connecting and reject private ranges (10.x, 172.16-31.x, 192.168.x, 127.x, ::1, link-local). Re-check after any redirects. * **Cap response size.** 5 KB maximum for metadata documents. * **Use aggressive timeouts.** 5 seconds connect, 10 seconds total. * **Rate limit per origin.** Prevent abuse from repeated fetch attempts. #### Domain Compromise Scope A natural concern: "What if an attacker gains control of a customer's domain?" The blast radius is more contained than it might appear. Even with full domain control, an attacker must still present tokens from a valid IDP. They could modify the CIMD to point to an IDP they control, but tokens from that IDP would only grant access to the organization associated with *that domain*. They cannot use a compromised domain to attack other organizations. Each org is isolated by its origin. The attacker is essentially attacking themselves. They control the domain, they control the CIMD, they control the IDP, and they get access to... an organization scoped to their own domain. This is the same trust model as HTTPS itself: domain ownership is the root of trust. #### Token Lifecycle as Defense The short-lived nature of OAuth tokens provides defense-in-depth: * **ID tokens expire quickly.** Best practice is 5-60 minutes. Even if a CIMD is compromised, existing user sessions expire naturally within this window. * **Refresh tokens rotate.** Modern IDPs issue new refresh tokens with each use. A stolen refresh token becomes invalid after one use. Many IDPs also detect reuse and revoke the entire token family. * **CIMD changes invalidate sessions.** If a domain changes hands and the new owner modifies the CIMD, existing tokens fail validation immediately. The issuer or audience no longer matches. There's no persistent access that survives a CIMD change. The attack window is bounded by token lifetime, not by how long it takes someone to notice the compromise. #### Edge Cases Worth Noting **Subdomain takeover:** If a customer has dangling DNS records pointing to decommissioned services (e.g., old Heroku or S3 deployments), an attacker could claim that subdomain and host a malicious CIMD. This is a real attack vector, but it's a general web security issue, not specific to CIMD. The mitigation is the same: customers should audit their DNS records and remove dangling entries. **Domain expiration:** If a customer's domain expires and someone else registers it, the new owner could claim the auto-provisioned organization. This is concerning for abandoned deployments. Mitigation options include: * Periodic re-validation of domain ownership * Alerting on CIMD changes (especially IDP changes) * Requiring explicit confirmation for significant configuration changes **First-user-as-admin race:** If using `admin_contact` with an email, an attacker who knows that email and can obtain a valid token for it could beat the legitimate admin to claiming the org. This is a narrow window (only during initial provisioning) and requires the attacker to have legitimate IDP access. Low risk, but worth documenting. #### What We're Trusting To summarize the trust model: | We Trust | Because | | ------------------------------------ | -------------------------------------- | | Domain ownership proves legitimacy | HTTPS + DNS (same as all web security) | | CIMD content is authoritative | Only the domain owner can host it | | IDP tokens prove user identity | Cryptographic signatures we verify | | Token audience prevents confusion | Standard OIDC claim validation | | Short token lifetimes limit exposure | Industry best practice, IDP-enforced | This isn't a novel trust model. It's composition of existing, battle-tested mechanisms. *** ## Relationship to OpenID Connect This proposal occupies similar territory to existing OIDC specs, but it's solving a narrower problem, which is why it can be simpler. ### What OIDC Federation Solves [OpenID Connect Federation 1.0](https://openid.net/specs/openid-connect-federation-1_0-21.html) addresses a broad challenge: how can an Identity Provider accept authentication requests from Relying Parties it has never seen before? The answer is sophisticated. Trust anchors issue signed statements about entities. Intermediate authorities form chains. RPs publish Entity Statements (signed JWTs) containing their metadata. When an RP initiates auth, the IDP walks the trust chain, validates signatures, applies federation policies, and decides whether to proceed. This machinery exists because OIDC Federation enables the *full authentication flow*: authorization requests, token issuance, consent, scopes. The IDP is actively participating. It needs strong guarantees before issuing tokens to an unknown party. ```mermaid theme={null} graph LR subgraph "OIDC Federation" RP[Relying Party] -->|Auth Request| IDP[Identity Provider] IDP -->|Discovers| RPMeta[RP Entity Statement] IDP -->|Validates| Chain[Trust Chain] IDP -->|Issues| Tokens[Tokens] end ``` ### What We're Actually Solving Our problem is narrower: **how can a third party know which IDP to validate tokens against, without pre-registration?** We're not participating in the auth flow. The customer's IDP already issued the token. The user already authenticated. We just need to validate what we received. We're asking a simpler question: "which IDP does this domain use?" ```mermaid theme={null} graph LR subgraph "Our Proposal" User -->|Already authenticated| IDP[Customer's IDP] IDP -->|Token issued| User User -->|Token + Origin| Char Char -->|Discovers| CIMD[Customer's CIMD] Char -->|Fetches JWKS| IDP Char -->|Validates| Token[Token] end ``` The IDP doesn't know we exist. We don't ask it for permission. We just fetch its public keys (which are public) and verify signatures. This is standard OIDC token validation. The only question is *which* IDP to validate against. ### Why Our Approach Is Simpler The simplicity isn't a shortcut. It's appropriate for the scope: | | OIDC Federation | This Proposal | | ---------------------- | --------------------------------------- | --------------------------------------- | | **Question answered** | "Should I issue tokens to this RP?" | "Which IDP issued this token?" | | **Who's deciding** | The IDP (active participant) | Us (passive validator) | | **What happens next** | Full auth flow, token issuance | Token validation only | | **Trust requirements** | IDP must trust RP before issuing tokens | We just need to know which IDP to check | | **Failure mode** | Auth blocked | Validation fails, user re-authenticates | OIDC Federation needs signed statements and trust chains because the IDP is making a consequential decision: issuing tokens that grant access. Getting that wrong has security implications. We're making a simpler decision: validating tokens that were already issued. If we get it wrong, the token fails validation. The user re-authenticates. There's no token leakage, no unauthorized access, just a failed validation. ### What We Build On To be precise about lineage: **We build on CIMD.** The core pattern (publish metadata at your domain, domain ownership = trust) comes directly from [OAuth Client ID Metadata Documents](https://datatracker.ietf.org/doc/draft-ietf-oauth-client-id-metadata-document/). We're extending CIMD with IDP configuration fields. **We use OIDC.** Token validation is standard OIDC. We fetch the IDP's JWKS via `/.well-known/openid-configuration`, verify signatures, check issuer and audience claims. Nothing novel here. **We occupy similar space as OIDC Federation.** Both enable dynamic discovery without pre-registration. But we're not building on Federation's trust chain model because we don't need it. ### Summary OIDC Federation is the right tool when IDPs need to trust unknown RPs before issuing tokens. For token validation by third parties, that machinery is overkill. Domain ownership is sufficient trust. The token itself carries cryptographic proof of the IDP's involvement. We're not asking the IDP to do anything, just reading its public keys. This proposal achieves dashboard-free SSO bootstrap for embedded agents by recognizing that token validation is a narrower problem than token issuance. *** ## Current Status This is exploratory work. CIMD itself is an IETF draft with growing adoption, but the `token_issuer` extension described here doesn't exist as a standard. I'm not actively building this, just documenting an idea that seemed worth sharing. If it resonates, or if you see problems with the approach, I'd be curious to hear. ## Further Reading How Char validates tokens from your IDP Current (dashboard-based) IDP configuration ## References * [OAuth Client ID Metadata Document (IETF OAuth WG Draft)](https://datatracker.ietf.org/doc/draft-ietf-oauth-client-id-metadata-document/) * [MCP Authorization Specification](https://modelcontextprotocol.io/specification/draft/basic/authorization) * [WorkOS: CIMD in MCP](https://workos.com/blog/client-id-metadata-documents-cimd-oauth-client-registration-mcp) * [Auth0: CIMD is the Future of MCP](https://auth0.com/blog/cimd-vs-dcr-mcp-registration/) # AI Resources Source: https://docs.usechar.com/vibe/index MCP endpoints, llms.txt links, skills commands, and source locations. Quick links for AI tooling resources. ## MCP Endpoints | Category | Resource | Link | | ------------ | --------------- | -------------------------------------------------------------------- | | `llms.txt` | Char Docs | [https://docs.usechar.ai/llms.txt](https://docs.usechar.ai/llms.txt) | | `llms.txt` | WebMCP Docs | [https://docs.mcp-b.ai/llms.txt](https://docs.mcp-b.ai/llms.txt) | | MCP endpoint | Char Docs MCP | [https://docs.usechar.ai/mcp](https://docs.usechar.ai/mcp) | | MCP endpoint | WebMCP Docs MCP | [https://docs.mcp-b.ai/mcp](https://docs.mcp-b.ai/mcp) | | MCP endpoint | Char SaaS MCP | [https://app.usechar.ai/mcp](https://app.usechar.ai/mcp) | ## Quick Commands ```bash theme={null} /plugin marketplace add WebMCP-org/char-plugin /plugin install char@webmcp /char:setup npx skills add vercel/ai ``` ## Locations | Resource | Location | | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Chrome DevTools MCP fork | [https://github.com/WebMCP-org/npm-packages/tree/main/packages/chrome-devtools-mcp](https://github.com/WebMCP-org/npm-packages/tree/main/packages/chrome-devtools-mcp) | | Char plugin | [https://github.com/WebMCP-org/char-plugin](https://github.com/WebMCP-org/char-plugin) | | Vercel AI resources | [https://vercel.com/docs/ai-resources](https://vercel.com/docs/ai-resources) | | AgentSkills spec | [https://agentskills.io](https://agentskills.io) | ## Related Manual Claude Code MCP setup. Manage your org from Claude Code. Create and update skills. SKILL.md format reference.