# Build with your AI > Put the AI you built on uare.ai on your website, in your own app, or in Claude, Cursor and any MCP client. ```html title="Website" ``` ```ts title="Public API" const API = "https://app.uare.ai/api/v1/public/ai/your-handle"; // Call this when a visitor sends their first message, from a page on a website allowed in Settings → Website async function startSession() { const res = await fetch(`${API}/sessions`, { method: "POST" }); return res.json(); // { session_id, token, ... } } ``` ```bash title="Your MCP" claude mcp add --transport http uare https://app.uare.ai/api/v1/mcp \ --header 'Authorization: Bearer ${UARE_API_KEY}' ``` ## Pick your integration Your AI on uare.ai learns how you think, write and decide. Here's where it can work outside uare.ai: | Integration | What it's for | Setup | Runs as | | --- | --- | --- | --- | | [Website widget](/docs/website/quickstart) | Your website's visitors chat with your AI, in a bubble or inside your page. | One line of HTML | Public mode, like your public profile | | [Public API](/docs/api/build-your-own) | A chat with your own design, on your site or in your app, with the Vercel AI SDK or plain JavaScript. | Allow your site, start a session | Public mode | | [uare.ai Experts MCP server](/docs/mcp/experts) | People in an assistant such as ChatGPT or Claude find you for what they need and talk to your AI there. | Listing, by invitation | Public mode, as a visitor | | [Your MCP server](/docs/mcp/quickstart) | You, using your AI from Claude, Cursor or VS Code. | An API key | You, with your private context | The first three serve visitors, in public mode, and need no key: you choose the websites that may show your AI, uare.ai lists the experts that assistants can find, and each visitor gets their own conversation. Your MCP server is for you alone. Its key carries your private context, so never use it to answer visitors. ## Start here - **For your website:** [allow your site and paste the snippet](/docs/website/quickstart). It takes about two minutes. - **For your own chat UI:** [start a session and stream replies](/docs/api/build-your-own). - **For assistants such as ChatGPT and Claude:** [see how the uare.ai Experts MCP server works](/docs/mcp/experts) and what it shows of you. - **For your own AI tools:** [create an API key](/docs/mcp/api-keys), then [connect your MCP client](/docs/mcp/quickstart) to your MCP server. New to uare.ai's vocabulary? [Concepts](/docs/concepts) explains your AI, who it talks to, and what each integration can reach. ## Built for AI agents too Every page here is also plain markdown. Add `.md` to a page's URL, for example [/docs/website/quickstart.md](/docs/website/quickstart.md), or request the page with an `Accept: text/markdown` header. [/llms.txt](/llms.txt) lists every page, and [/llms-full.txt](/llms-full.txt) holds all of them in one file. ## What's next More ways to bring your AI into your own products are on the way: signed-in visitors with their own history, and your courses, services and thoughts playing on your site. The [changelog](/docs/changelog) announces each one when it ships. --- # Concepts > The ideas behind every integration: your AI, who it talks to, which websites may show it, the two MCP servers, and what an API key lets another tool do. ## Your AI On uare.ai you build an AI from your knowledge, your voice and your skills. It keeps learning from what you upload and from your conversations with it. Every integration in these docs acts on that AI. ## Two audiences Your AI talks to two kinds of people, and behaves differently with each. - **You.** In your own chat, your AI works with your full private context: your notes, your connected apps and your preferences. - **Visitors.** On your public profile, on websites you allow and, if you're a listed expert, in assistants such as ChatGPT and Claude, your AI answers in public mode. It uses the knowledge you've made public and doesn't touch your connected apps. Each integration runs as one of the two: | Integration | Runs as | Who it's for | | --- | --- | --- | | [Website widget](/docs/website/quickstart) and [public API](/docs/api/build-your-own) | A visitor, in public mode | Anyone on the websites you allow | | [uare.ai Experts MCP server](/docs/mcp/experts) | A visitor, in public mode | People in assistants such as ChatGPT and Claude, if you're a listed expert | | [Your MCP server](/docs/mcp/quickstart) | You, with your private context | You, through your own tools | Your MCP server runs your AI **as you**. Treat your API key like a password, and never use it to answer other people: your AI would answer them with your private context. ## Two MCP servers uare.ai runs two MCP servers. Any MCP client can connect to either, but they act as different people: | | [Your MCP server](/docs/mcp/quickstart) | [uare.ai Experts MCP server](/docs/mcp/experts) | | --- | --- | --- | | Address | `https://app.uare.ai/api/v1/mcp` | `https://app.uare.ai/api/v1/public/mcp` | | Key | Your API key | None | | Runs as | You | A visitor | | Reaches | Your AI with your private context: your notes, connected apps, skills and preferences | Listed experts' public profiles, and their AIs' answers in public mode | | For | You, in Claude, Cursor, VS Code or your own code | People in assistants such as ChatGPT and Claude | Your key turns any MCP client into you, so keep it to yourself. The Experts server takes no key and reaches nothing private, for anyone. ## Allowed websites Your AI answers the website widget and the public API only on the websites you list in [Settings → Website](https://app.uare.ai/chat-with-me?settings=website). The list starts empty, so nothing can embed your AI until you add a site. The list lives in your own Settings, so you, the AI's owner, make every change to it, even when someone else builds the site. Entries are addresses such as `https://example.com`, or `https://*.example.com` for every subdomain. The list decides where your AI can appear. It isn't a password: it stops other websites from putting your AI on their pages, not someone writing their own code from scratch. That's why visitors only ever reach your public mode. ## Visitor conversations Every visitor gets their own conversation. Your AI remembers what was said in it while it lasts: up to two hours, or until an hour passes without messages. A conversation includes 10 messages. After that, visitors can continue on uare.ai, where they can sign up and keep talking to your AI. Conversations through the uare.ai Experts MCP server follow the same rules. Since that server keeps no session, the assistant carries each conversation from one message to the next with its [`consult` value](/docs/mcp/experts#keep-a-conversation-going). ## Skills and preferences Skills are markdown files you write for your AI: tone guidelines, rules for your field, example conversations. Preferences are rules your AI always applies to its own behavior. An agent connected to your MCP server can read both with [`list_skills`](/docs/mcp/tools#list_skills) and [`get_skill`](/docs/mcp/tools#get_skill), so it can work the way you would. ## Connected apps You can connect apps such as Gmail or Google Drive to your AI in uare.ai. Through your MCP server, the [`act`](/docs/mcp/tools#act) tool uses those connections to look things up or to prepare changes for you. ## Writes wait for your confirmation Reading through your connected apps happens right away. Anything that would send, create or delete waits: `act` returns a plan and a confirmation token, and only [`confirm_action`](/docs/mcp/tools#confirm_action) carries it out. The token works once and expires after about 10 minutes. ## Experiences Courses, services and thoughts (articles, artifacts and audiobooks) are what your AI creates and runs for other people. Today they play on uare.ai: a visitor on your website can continue there from your AI's profile. --- # Add your AI to your website > Put a chat with your AI on your own site with one line of HTML. Visitors talk to it right there, in public mode. Your website's visitors can chat with your AI without leaving your page. It takes two steps: the AI's owner allows your site, then you paste one line of HTML. ```html ``` That line adds a chat bubble to the corner of every page it's on. Visitors open it and talk to your AI the way they would on your public profile. ## 1. Allow your website Your AI only answers on websites listed in its owner's Settings on uare.ai. The AI's owner does this step, signed in to their account: if you're building the site for someone else, send them the addresses to add. 1. Open [Settings → Website](https://app.uare.ai/chat-with-me?settings=website) in uare.ai. 2. Add your site's address, for example `https://example.com`. Add `https://*.example.com` as well to cover every subdomain, such as `www`. 3. To try it on your computer first, add the local address you use, for example `http://localhost:3000`. Changes take effect within a minute. > [!IMPORTANT] > Your AI's profile must be public. Websites can't show a private AI, and the Website page tells you if yours is private. ## 2. Paste the snippet The Website page shows your snippet with your handle filled in. Your handle is the last part of your public profile's address: for `https://app.uare.ai/p/your-handle`, it's `your-handle`. If your address ends in a long ID instead of a name, that ID is your handle. Paste the snippet into your site's HTML, just before the closing `` tag, on every page that should show the bubble. Most site builders have a place for it: | Platform | Where the snippet goes | | --- | --- | | WordPress | The footer section of a header-and-footer code plugin | | Webflow | Site settings → Custom code → Footer code | | Shopify | Your theme's `theme.liquid`, just before `` | | Squarespace | Code injection, in the Footer box | | Wix | Custom code, placed at the end of the body | | Framer | Site settings → Custom code, at the end of the body | | Next.js or React | See [React and Next.js](/docs/website/customize#react-and-nextjs) | Open your site. The bubble appears in the bottom-right corner. ## Put the chat inside your page instead To show the conversation inside your layout instead of in a bubble, load the script once, then place a `` element where it should go and give it a height: ```html ``` [Customize the widget](/docs/website/customize) covers colours, position, the greeting, and the JavaScript events your page can listen to. If your site builder only accepts an iframe, see [Embed with an iframe](/docs/website/customize#embed-with-an-iframe). ## What visitors get - **Public mode.** Your AI answers from the knowledge you've made public and doesn't touch your private notes or connected apps, exactly as on your public profile. - **Free messages.** Each conversation includes 10 messages. After that, the widget offers to continue on uare.ai, where visitors can sign up and keep talking to your AI. - **Memory across pages.** The conversation follows visitors around your site. It ends after an hour without messages, or two hours after it started, and a new chat starts with a fresh allowance. - **No cookies.** The widget stores the conversation in your site's browser storage and sends no cookies to uare.ai. ## Troubleshooting | What you see | What to do | | --- | --- | | No bubble, and the browser console says the site isn't on the list of allowed websites | The AI's owner adds the exact address from your browser's address bar in Settings → Website. `https://example.com` doesn't cover `https://www.example.com`: add both, or add `https://*.example.com`. | | No bubble, and the console says no public AI answers to your handle | Check the handle against your public profile's address, and that your profile is public. | | The console mentions a Content Security Policy | Your site restricts which scripts and servers its pages may use. Add `https://app.uare.ai` to `script-src` and `connect-src`. See [Content Security Policy](/docs/website/customize#content-security-policy). | | It works on your computer but not on your live site | Your live address isn't on the list yet: allowing `localhost` doesn't allow your domain. | ## Next steps - [Customize the widget](/docs/website/customize): attributes, colours, events and methods. - [Build your own chat UI](/docs/api/build-your-own): the same conversation through the public API, for a design that's entirely yours. --- # Customize the website widget > Attributes, colours, JavaScript methods and events for the uare.ai chat widget, plus React, Next.js, iframe and Content Security Policy setup. The widget is one script that defines a `` HTML element. You can configure it two ways: - **On the script tag**, with `data-` attributes. The script then adds a chat bubble to the page by itself: ```html ``` - **On a `` element** you place yourself, which is how you put the chat inside your layout. Load the script once per page: ```html ``` Your website must be on your AI's list of allowed websites, either way. The AI's owner adds it, in their Settings on uare.ai: see [Allow your website](/docs/website/quickstart#1-allow-your-website). ## Attributes | Attribute | Values | Default | | --- | --- | --- | | `handle` | Your AI's handle, from your public profile's address. Required. | | | `mode` | `bubble`: a button in the corner that opens the chat. `inline`: the chat fills the element, which you size. | `bubble` | | `theme` | `auto` follows the visitor's light or dark setting, or force `light` or `dark`. | `auto` | | `accent-color` | Any CSS colour, for the bubble, the send button and the visitor's messages. The text on it turns black or white to stay readable. When your AI has an avatar, the avatar fills the bubble, so the colour shows only on the send button and the visitor's messages. | Near-black, or near-white in dark mode | | `position` | `bottom-right` or `bottom-left`, for the bubble. | `bottom-right` | | `greeting` | The line shown before the first message. | Your AI's bio | | `placeholder` | The message box's placeholder. | `Message …` | | `open` | Present to start with the bubble open. On the element, `chat.open = true` or `false` opens or closes the bubble, and `chat.open()` still works: see [JavaScript](#javascript). | Closed | | `lang` | The visitor's language, as a tag such as `fr` or `es`. It's set on the chat for screen readers and kept with the conversation. Your AI picks the language of its replies itself, and the widget's own labels stay in English: use `greeting` and `placeholder` for the words visitors read first. | `en` | On the script tag, add `data-` to each name: `data-accent-color="#7c3aed"`. You can change attributes on the element at any time and it updates. ## Colours and fonts The widget lives in its own shadow DOM, so your site's CSS doesn't leak into it. To restyle it, set these CSS variables on `uare-chat`: ```css uare-chat { --uare-accent: #7c3aed; /* bubble (when your AI has no avatar), send button, visitor messages */ --uare-accent-fg: #ffffff; /* text on the accent */ --uare-font: "Inter", system-ui, sans-serif; /* defaults to your page's font */ --uare-radius: 20px; /* corners of the chat panel */ --uare-bg: #ffffff; /* panel background */ --uare-fg: #18181b; /* text */ --uare-surface: #f4f4f5; /* your AI's messages */ --uare-muted: #6b6b72; /* secondary text */ --uare-border: #e4e4e7; /* lines and outlines */ } ``` Variables you set apply in both light and dark mode. Leave one unset to keep the widget's default for the visitor's theme. ## JavaScript Methods on the element: | Method | What it does | | --- | --- | | `open()` | Opens the bubble. In inline mode, focuses the message box. | | `close()` | Closes the bubble. | | `toggle()` | Opens or closes the bubble. | | `send(text)` | Sends `text` as the visitor's message. Returns a promise that resolves when the reply has finished, and rejects when the message isn't sent or the reply fails. | | `reset()` | Starts a new conversation. | `open` is also a property that takes a boolean, the way frameworks such as React 19 set it: `chat.open = true` opens the bubble and adds the `open` attribute, `chat.open = false` closes it and removes the attribute. Reading `chat.open` still gives the method. When `send()` doesn't send, or the reply fails, its promise rejects with an `Error` whose `code` says why: | `code` | Why | | --- | --- | | `not_ready` | Your AI hasn't loaded yet, or couldn't load. Call `send()` after `uare:ready`. | | `turn_in_progress` | A reply is still being written. Send the next message once the previous `send()` has settled. | | `invalid_request` | The text is empty. | | `message_limit_reached` | The conversation's free messages are used up. The error's `continue_url` is your AI's page on uare.ai, and `uare:limit` fires too. | | Any other | The reply failed. `code` and `message` are the ones `uare:error` reports, such as `chat_unavailable`. | If the visitor stops the reply, or `reset()` starts a new conversation while it's streaming, the promise resolves. Events, which bubble up to `document`. Each one's `detail` is described here: | Event | `detail` | | --- | --- | | `uare:ready` | `{ ai: { handle, name, avatar_url, bio, profile_url } }` when your AI has loaded: once per load, so again only if you change `handle` or `api-base` | | `uare:open`, `uare:close` | `{}` | | `uare:message` | `{ role: "user" \| "assistant", text }` for each finished message | | `uare:limit` | `{ limit, continue_url }` when the conversation's free messages are used up | | `uare:error` | `{ code, message }` when the widget can't load or a reply fails | For example, open the chat from your own button and send a first question. This works with the one-line snippet, which adds `` to the page only once its script has loaded, and with an element you place yourself: ```html ``` `send()` opens the bubble if it's closed. Call it after `uare:ready`: before your AI has loaded, it rejects with `not_ready` and sends nothing. ## React and Next.js React 19 renders custom elements directly. Load the script once with `next/script`, then use the element like any other: ```tsx title="Next.js" "use client"; import Script from "next/script"; import { useEffect, useRef } from "react"; export function AiChat() { const ref = useRef(null); useEffect(() => { const chat = ref.current; if (!chat) return; const onMessage = (event: Event) => { const { role, text } = (event as CustomEvent<{ role: string; text: string }>).detail; console.log(role, text); }; chat.addEventListener("uare:message", onMessage); return () => chat.removeEventListener("uare:message", onMessage); }, []); return ( <> ``` ```html title="The iframe" ``` The widget uses the page's font, so give `chat.html` the font you want. The website that has to be on your list is the one serving `chat.html`. The page holding the iframe can be on any site, because the chat talks to uare.ai from inside the frame. Browsers keep an iframe's storage separate for each website that frames it, so the conversation follows visitors from page to page on one website, and another website framing the same `chat.html` starts a conversation of its own. > [!NOTE] > Don't frame your public profile (`https://app.uare.ai/p/your-handle`). It can load in an iframe on `localhost` while you build, but browsers block it on your live domain. ## Content Security Policy If your site sends a `Content-Security-Policy` header, allow uare.ai to serve the script and answer the chat: ```http Content-Security-Policy: script-src 'self' https://app.uare.ai; connect-src 'self' https://app.uare.ai; img-src 'self' https: ``` `img-src` needs to cover your AI's avatar. `https:` allows it wherever it's hosted. The widget's styles need nothing: they're applied from the script, so `style-src` doesn't have to allow inline styles. ## Privacy - The widget sends no cookies and sets none. The conversation lives in your site's `localStorage`, under keys starting with `uare-chat:`, and leaves when visitors start a new chat or it expires. - Messages go to uare.ai and your AI answers them in public mode, as on your public profile. uare.ai sees the visitor's IP address, which it uses for rate limits and visitor counts. - Mention uare.ai in your privacy policy as a service that processes the chat messages visitors send. ## Versions `/embed/v1.js` stays compatible: updates add features without breaking the attributes, methods and events above. A breaking change would ship as `/embed/v2.js`. --- # Build your own chat UI > Use the public chat API to put your AI in a chat you design yourself, with the Vercel AI SDK or a few lines of plain JavaScript. The [website widget](/docs/website/quickstart) runs on a public API, and so can your own chat. Use it when you want the conversation in your design system, inside an existing app, or in a UI that's entirely yours. It takes two calls from your web page: 1. **Start a session** when the visitor sends their first message. `POST /sessions` returns a session token for that visitor's conversation. 2. **Send messages.** `POST /chat`, with the token in an `X-Uare-Session` header, streams your AI's reply back as it's written. ```text https://app.uare.ai/api/v1/public/ai/your-handle ``` There's no API key. Your AI answers web pages on the [websites its owner allows](/docs/website/quickstart#1-allow-your-website), in public mode, the way it answers visitors on its public profile. The owner adds them in their Settings on uare.ai, `http://localhost:3000` or whichever local address you use included, to try it while you build. The reply streams in the [AI SDK](https://ai-sdk.dev) UI message format, so `useChat` reads it directly. Anything that reads server-sent events can read it too. ## React with the AI SDK Install the AI SDK: ```bash npm install ai @ai-sdk/react ``` Then give `useChat` a transport whose headers start the session the first time the visitor sends a message, and reuse it after that: ```tsx title="Chat.tsx" "use client"; import { useChat } from "@ai-sdk/react"; import { DefaultChatTransport } from "ai"; import { useRef, useState } from "react"; const API = "https://app.uare.ai/api/v1/public/ai/your-handle"; type Session = { session_id: string; token: string }; async function startSession(): Promise { const res = await fetch(`${API}/sessions`, { method: "POST" }); const body = await res.json(); if (!res.ok) throw new Error(body.error?.message ?? `The chat couldn't start (${res.status}).`); return body; } export function Chat() { const [input, setInput] = useState(""); const session = useRef | null>(null); const { messages, sendMessage, status, error } = useChat({ transport: new DefaultChatTransport({ api: `${API}/chat`, // No session until the visitor's first message; every message after it reuses the session. headers: async () => { session.current ??= startSession().catch((err) => { session.current = null; // the next message tries again throw err; }); return { "X-Uare-Session": (await session.current).token }; }, }), }); const busy = status === "submitted" || status === "streaming"; return (
{messages.map((message) => (

{message.role === "user" ? "You" : "AI"}: {message.parts.map((part, i) => (part.type === "text" ? {part.text} : null))}

))} {error ?

{error.message}

: null}
{ e.preventDefault(); sendMessage({ text: input }); setInput(""); }} > setInput(e.target.value)} disabled={busy} />
); } ``` Your AI's replies often use markdown. Render text parts with a markdown component, such as [Streamdown](https://streamdown.ai), for headings, lists and links. The stream also carries your AI's suggested follow-ups as a `data-suggestions` part, and transient `data-quota` and `data-status` parts you can read with `useChat`'s `onData` callback. [Stream parts](/docs/api/reference#stream-parts) lists them all. ## Plain JavaScript No library needed: a form, and a script that starts a session with the visitor's first message, reads the stream and handles each `data:` line. ```html title="index.html"

``` ```js title="chat.js" const API = "https://app.uare.ai/api/v1/public/ai/your-handle"; // The session starts with the visitor's first message; later messages reuse it. let session = null; async function sessionToken() { session ??= fetch(`${API}/sessions`, { method: "POST" }).then(async (res) => { const body = await res.json(); if (!res.ok) throw new Error(body.error?.message ?? `The chat couldn't start (${res.status}).`); return body.token; }); try { return await session; } catch (error) { session = null; // the next message tries again throw error; } } async function ask(text, onText) { const res = await fetch(`${API}/chat`, { method: "POST", headers: { "content-type": "application/json", "x-uare-session": await sessionToken() }, body: JSON.stringify({ text }), }); if (!res.ok) { const body = await res.json().catch(() => null); throw new Error(body?.error?.message ?? `The chat failed (${res.status}).`); } const reader = res.body.getReader(); const decoder = new TextDecoder(); let buffer = ""; for (;;) { const { done, value } = await reader.read(); if (done) return; buffer += decoder.decode(value, { stream: true }); let end; while ((end = buffer.indexOf("\n\n")) !== -1) { const line = buffer.slice(0, end); buffer = buffer.slice(end + 2); if (!line.startsWith("data: ")) continue; const data = line.slice(6); if (data === "[DONE]") return; const part = JSON.parse(data); if (part.type === "text-delta") onText(part.delta); if (part.type === "error") throw new Error(part.errorText); } } } document.querySelector("#chat").addEventListener("submit", async (event) => { event.preventDefault(); const input = document.querySelector("#message"); const reply = document.querySelector("#reply"); const text = input.value.trim(); if (!text) return; input.value = ""; reply.textContent = ""; try { await ask(text, (delta) => reply.append(delta)); } catch (error) { reply.textContent = error.message; } }); ``` This page shows the reply as plain text: your AI's replies often use markdown, and its marks (`**bold**`, `- ` lists, links) appear as typed. To format them, render the text with a markdown library and sanitize the HTML it produces before you put it on the page, because the reply isn't text your page wrote. ## Try it from a terminal The API reads the `Origin` header a browser sends. Set it yourself to an allowed website to try the API with `curl`: ```bash API=https://app.uare.ai/api/v1/public/ai/your-handle TOKEN=$(curl -s -X POST "$API/sessions" -H "Origin: http://localhost:3000" | sed -E 's/.*"token":"([^"]+)".*/\1/') curl -N -X POST "$API/chat" \ -H "Origin: http://localhost:3000" \ -H "X-Uare-Session: $TOKEN" \ -H "Content-Type: application/json" \ -d '{"text": "Hi! Who are you?"}' ``` The allowed-websites list decides which sites can show your AI. It isn't a password: anyone can set an `Origin` header outside a browser, so it can't make a conversation private. ## Bring back a conversation A session lasts up to two hours, and ends after an hour without messages. To show a visitor's conversation again after a page reload, keep the session and token, for example in `sessionStorage`, and load the messages: ```js const { messages } = await fetch(`${API}/sessions/${sessionId}/messages`, { headers: { "x-uare-session": token }, }).then((res) => res.json()); ``` They come back as AI SDK `UIMessage` objects, ready for `useChat({ id: sessionId, messages, transport })`. When a session expires, `POST /chat` returns `404 session_expired`: start a new session. Your AI remembers the conversation within a session, not across sessions. ## Limits - Each conversation includes 10 visitor messages. After that, `POST /chat` returns `429 message_limit_reached` with a `continue_url` to your AI's profile on uare.ai, where the visitor can keep going. - Regenerating a reply (`"trigger": "regenerate-message"`, `useChat`'s `regenerate()`) uses up a free message, like sending one. - Messages are limited to 4,000 characters. - A conversation answers one message at a time. Sending another while a reply is still streaming returns `409 turn_in_progress`. - Rate limits apply per visitor IP address. They're listed in the [reference](/docs/api/reference#limits), and every response says how many requests are left in its [`RateLimit-*` headers](/docs/api/reference#rate-limit-headers). ## Next steps - [Public API reference](/docs/api/reference): every endpoint, field, stream part and error. - [Customize the website widget](/docs/website/customize), if the ready-made chat fits after all. --- # Public API reference > Endpoints, fields, stream parts, errors and limits of the uare.ai public chat API. ```text https://app.uare.ai/api/v1/public/ai/{handle} ``` `{handle}` is the last part of your AI's public profile address. The API answers web pages on the websites your AI's owner allows in [Settings → Website](https://app.uare.ai/chat-with-me?settings=website). It needs no API key: a visitor's session token, sent in an `X-Uare-Session` header, authorizes their own conversation and nothing else. ## Endpoints | Method | Path | Does | | --- | --- | --- | | `GET` | `/api/v1/public/ai/{handle}` | Returns the AI's public card | | `POST` | `/api/v1/public/ai/{handle}/sessions` | Starts a conversation and returns its token | | `POST` | `/api/v1/public/ai/{handle}/chat` | Sends a message and streams the reply | | `GET` | `/api/v1/public/ai/{handle}/sessions/{session_id}/messages` | Returns the conversation so far | ## Get the AI `GET /api/v1/public/ai/{handle}` ```json { "ai": { "handle": "your-handle", "name": "Raj", "avatar_url": "https://…/avatar.png", "bio": "Makes hard ideas simple.", "profile_url": "https://app.uare.ai/p/your-handle" }, "quota": { "limit": 10 } } ``` `quota.limit` is how many messages a visitor can send in one conversation. Use this call to show your AI's name and picture before a visitor starts typing. It starts nothing. Like every endpoint here, it answers only a request with an `Origin` header naming an allowed website, which browsers add for you. Code on your server, such as a Next.js server component or server-side rendering, sends no `Origin` and gets `403 origin_required`. Fetch the card from the page instead, or send one of the allowed websites as the `Origin` header from your server. ## Start a session `POST /api/v1/public/ai/{handle}/sessions` The body is optional: | Field | Type | Description | | --- | --- | --- | | `language` | string | The visitor's language, as a tag such as `en` or `fr-CA`. Defaults to `en`. It's kept with the session; your AI picks the language of its replies itself. | Returns `201`: ```json { "session_id": "8f2c…", "token": "eyJhbGciOiJIUzI1NiJ9…", "expires_at": "2026-10-16T21:04:12.000Z", "quota": { "limit": 10, "remaining": 10 }, "ai": { "handle": "your-handle", "name": "Raj", "avatar_url": "https://…", "bio": "…", "profile_url": "…" } } ``` Start a session when a visitor begins chatting, not on every page view. The token belongs to that one visitor's conversation: don't share it between visitors. It expires at `expires_at`, two hours after it was issued. The session can end sooner, after an hour without messages, and then `POST /chat` returns `404 session_expired`. ## Send a message `POST /api/v1/public/ai/{handle}/chat` ```http X-Uare-Session: Content-Type: application/json ``` Send either a plain message: ```json { "text": "What do you help people with?" } ``` or the body the AI SDK's `DefaultChatTransport` sends, which `useChat` produces: ```json { "id": "8f2c…", "trigger": "submit-message", "messages": [ { "id": "m1", "role": "user", "parts": [{ "type": "text", "text": "What do you help people with?" }] } ] } ``` The API reads only the newest user message. The conversation's earlier turns come from the server, so a page can't rewrite them. With `"trigger": "regenerate-message"`, your AI answers the last message again, replacing its previous reply. A regenerated reply uses up one of the conversation's free messages, like a new message. The response is `text/event-stream`, with the header `x-vercel-ai-ui-message-stream: v1`. Each event is one `data:` line holding a JSON part, and the stream ends with `data: [DONE]`: ```text data: {"type":"start","messageId":"msg_4b1c…"} data: {"type":"data-quota","data":{"limit":10,"remaining":9},"transient":true} data: {"type":"text-start","id":"txt_9a…"} data: {"type":"text-delta","id":"txt_9a…","delta":"I help people "} data: {"type":"text-delta","id":"txt_9a…","delta":"make hard ideas simple."} data: {"type":"text-end","id":"txt_9a…"} data: {"type":"data-suggestions","data":{"items":[{"label":"What's your approach?","prompt":"What's your approach?"}]}} data: {"type":"finish"} data: [DONE] ``` ### Stream parts | Part | Data | Meaning | | --- | --- | --- | | `start` | `messageId` | The reply begins. | | `data-quota` | `{ limit, remaining }` | Messages left in this conversation after this one. Transient: not stored in the message. | | `data-status` | `{ label }` | What your AI is doing, such as `Thinking back...`. Transient. | | `text-start`, `text-delta`, `text-end` | `id`, `delta` | The reply's text, in order. Join the deltas. | | `data-suggestions` | `{ items: [{ label, prompt }] }` | Follow-ups to offer the visitor. Send `prompt` when one is picked. | | `data-limit` | `{ limit, continue_url }` | The free messages are used up. Followed by an `error` part. | | `error` | `errorText` | The reply failed. The text is safe to show the visitor. | | `finish` | | The reply is complete. | Ignore part types you don't recognise: new ones may be added. ## Get the conversation `GET /api/v1/public/ai/{handle}/sessions/{session_id}/messages`, with `X-Uare-Session: `. ```json { "messages": [ { "id": "8f2c…:0", "role": "user", "parts": [{ "type": "text", "text": "What do you help people with?" }] }, { "id": "8f2c…:1", "role": "assistant", "parts": [{ "type": "text", "text": "I help people make hard ideas simple." }] } ] } ``` The messages are AI SDK `UIMessage` objects, so `useChat({ messages })` can show them again after a reload. ## Errors Errors before a stream starts come back as JSON with a matching status: ```json { "error": { "code": "origin_not_allowed", "message": "https://example.com isn't on Raj's list of allowed websites. …" } } ``` | Status | `code` | What it means | | --- | --- | --- | | `400` | `invalid_request` | The body isn't valid: an empty message, one over 4,000 characters, or bad JSON. | | `401` | `session_token_required` | No `X-Uare-Session` header. | | `401` | `invalid_session_token` | The token is malformed or expired. Start a new session. | | `403` | `origin_required` | The request has no `Origin` header: it came from a server or a terminal, not a web page. Call the API from a web page, or set `Origin` to an allowed website. | | `403` | `origin_not_allowed` | The page's website isn't on the AI's list of allowed websites. | | `403` | `session_mismatch` | The token belongs to another session or another AI. | | `404` | `ai_not_found` | No public AI answers to this handle. Its profile may be private. | | `404` | `session_expired` | The session ended. Start a new one. | | `409` | `turn_in_progress` | A reply is still streaming in this conversation. Send the next message when it has finished. | | `429` | `message_limit_reached` | The conversation's free messages are used up. The body adds `limit` and `continue_url`. | | `429` | | Too many requests from this address. Wait the seconds in `Retry-After`, then retry. See [Rate limit headers](#rate-limit-headers). | | `503` | `chat_unavailable` | The AI can't take visitor chats right now. | Errors after the stream starts arrive as an `error` part, and the stream ends. ## Limits | Limit | Value | | --- | --- | | Messages per conversation | 10 (`quota.limit`) | | Message length | 4,000 characters | | New sessions | 20 per minute per IP address | | Messages | 30 per minute per IP address | | All public endpoints, apart from the [Experts MCP server](/docs/mcp/experts#limits) | 120 requests per minute per IP address | | Session lifetime | 2 hours, or 1 hour without messages | | Allowed websites per AI | 20 | Regenerating a reply counts toward the messages per conversation. ### Rate limit headers Every response carries `RateLimit-*` headers for the per-minute limit that applies to it: 20 for `POST /sessions`, 30 for `POST /chat`, and 120 for the other endpoints. ```http RateLimit-Policy: 30;w=60 RateLimit-Limit: 30 RateLimit-Remaining: 29 RateLimit-Reset: 60 ``` `RateLimit-Remaining` is how many more of those requests this IP address can make in the current minute, and `RateLimit-Reset` how many seconds to wait, 60 at most, for the count to start over. A `429` adds `Retry-After`, in seconds. Browsers don't show a page on another website the `RateLimit-*` headers: read those from your server or a terminal. A page can read `Retry-After` on a `429` from `POST /sessions` or `POST /chat`. Past the 120-request limit, the `429` comes without CORS headers, so the page sees a failed request instead: wait a minute before retrying. ## CORS The API answers cross-origin requests from any page and sends no cookies. Preflight requests always succeed, allowing the `Content-Type` and `X-Uare-Session` headers. The real request then checks the page's `Origin` against the AI's allowed websites, and a refused page gets a readable `403` explaining why. --- # Connect your AI to Claude, Cursor and more > Give Claude, Cursor, VS Code or any MCP client access to your uare.ai AI in about two minutes. Your MCP server lets your AI work inside the tools you already use, as you. It's a remote server that speaks the streamable HTTP transport and authenticates with your personal API key: ```text https://app.uare.ai/api/v1/mcp ``` > [!NOTE] > This server is for you alone. For people in assistants such as ChatGPT and Claude, uare.ai runs a separate server with no key, the [uare.ai Experts MCP server](/docs/mcp/experts), for the experts it lists. [Two MCP servers](/docs/concepts#two-mcp-servers) compares them. ## 1. Create an API key 1. Open [Settings → API keys](https://app.uare.ai/chat-with-me?settings=api-keys) in uare.ai. Settings also opens with ⌘, on a Mac or Ctrl+, elsewhere. 2. Select **Generate new key** and name it after the client you'll use, for example *Claude Desktop*. 3. Copy the key. > [!WARNING] > The key is shown only once. Anyone who has it can use your AI with your private context, so keep it somewhere safe and never commit it. Keys start with `uare_pk_`. [API keys](/docs/mcp/api-keys) covers revoking and replacing them. ## 2. Add the server to your client Pick your client. Claude Code, Cursor and VS Code read the key from a `UARE_API_KEY` environment variable or a secure prompt, so it never sits in their config. Claude Desktop needs the key in its config file, so keep that file private. ```bash title="Claude Code" claude mcp add --transport http uare https://app.uare.ai/api/v1/mcp \ --header 'Authorization: Bearer ${UARE_API_KEY}' ``` ```json title="Cursor" { "mcpServers": { "uare": { "url": "https://app.uare.ai/api/v1/mcp", "headers": { "Authorization": "Bearer ${env:UARE_API_KEY}" } } } } ``` ```json title="VS Code" { "inputs": [ { "type": "promptString", "id": "uare-api-key", "description": "uare.ai API key", "password": true } ], "servers": { "uare": { "type": "http", "url": "https://app.uare.ai/api/v1/mcp", "headers": { "Authorization": "Bearer ${input:uare-api-key}" } } } } ``` ```json title="Claude Desktop" { "mcpServers": { "uare": { "command": "npx", "args": [ "-y", "mcp-remote@latest", "https://app.uare.ai/api/v1/mcp", "--header", "Authorization: Bearer ${UARE_API_KEY}" ], "env": { "UARE_API_KEY": "uare_pk_your_key_here" } } } } ``` Where each one goes: - **Claude Code**: run the command in your terminal. Keep the single quotes: Claude Code stores `${UARE_API_KEY}` itself and reads the variable each time it connects, so `export UARE_API_KEY=uare_pk_...` in the shell you start it from. With double quotes, your shell would write the key into Claude Code's config. - **Cursor**: `~/.cursor/mcp.json` for every project, or `.cursor/mcp.json` in one project. Cursor reads `${env:UARE_API_KEY}` from the environment it was started in, so a Cursor opened from the Dock won't see a key exported in your shell. In that case, put the key directly in the header and keep the file out of version control. - **VS Code**: `.vscode/mcp.json` in your workspace. VS Code asks for the key the first time the server starts and stores it securely. - **Claude Desktop**: Settings, then **Developer**, then **Edit Config**. Restart Claude Desktop after saving. Claude Desktop connects through [`mcp-remote`](https://www.npmjs.com/package/mcp-remote), which needs Node.js. On Windows, see below. ### Claude Desktop on Windows On Windows, Claude Desktop doesn't escape the spaces inside `args` when it starts `npx`, so mcp-remote gets the `Authorization` header in pieces. mcp-remote's own workaround is a header argument with no spaces, with the whole value, `Bearer` included, in the environment variable: ```json { "mcpServers": { "uare": { "command": "npx", "args": [ "-y", "mcp-remote@latest", "https://app.uare.ai/api/v1/mcp", "--header", "Authorization:${AUTH_HEADER}" ], "env": { "AUTH_HEADER": "Bearer uare_pk_your_key_here" } } } } ``` Keep `Authorization:${AUTH_HEADER}` exactly as it is, with no space after the colon. > [!TIP] > When you generate a key in uare.ai, the dialog also offers a ready-to-paste config for Claude Desktop and Cursor with your key filled in. On Windows, change its Claude Desktop header as shown above. ## 3. Try it Ask your client to use uare, for example: - "Use uare to draft a LinkedIn post about what we learned launching our beta." - "Ask my uare AI how it would reply to this email: …" - "Use uare's decide tool: should I take the offer from A or from B?" - "Ask uare to find the latest email from my accountant." The client picks the matching tool: [`generate_post`](/docs/mcp/tools#generate_post), [`respond_as_me`](/docs/mcp/tools#respond_as_me), [`decide`](/docs/mcp/tools#decide) or [`act`](/docs/mcp/tools#act). ## Use the tools from your own code Your own server can call the tools too, for features only you use, such as a "draft a post in my voice" button in your site's admin. Connect with an MCP SDK and your key. In Node.js, with the official SDK: ```bash npm install @modelcontextprotocol/sdk ``` ```js title="draft-post.mjs" import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; const client = new Client({ name: "my-site-admin", version: "1.0.0" }); await client.connect( new StreamableHTTPClientTransport(new URL("https://app.uare.ai/api/v1/mcp"), { requestInit: { headers: { Authorization: `Bearer ${process.env.UARE_API_KEY}` } }, }), ); const result = await client.callTool({ name: "generate_post", arguments: { topic: "What we learned launching our beta", platform: "linkedin" }, }); // generate_post answers with one text block holding its JSON result. const { post } = JSON.parse(result.content[0].text); console.log(post); await client.close(); ``` Results come back as JSON in text blocks: one block for a single result, one block per entry for a list such as `list_skills`. The [tools reference](/docs/mcp/tools) lists each tool's fields. > [!CAUTION] > Run this on your server, behind your own sign-in. The key acts as you, with your private context: never send it to a browser, and never use these tools to answer your website's visitors. Visitors get the [website widget](/docs/website/quickstart), the [public API](/docs/api/build-your-own) or, in assistants, the [uare.ai Experts MCP server](/docs/mcp/experts). ## Limits - 120 requests per minute for each key owner, and 600 per minute from one network address before authentication. - Changes through your connected apps wait for [`confirm_action`](/docs/mcp/tools#confirm_action). A confirmation token works once and expires after about 10 minutes. ## Next steps - [Tools reference](/docs/mcp/tools): every tool, its parameters and what it returns. - [Troubleshooting](/docs/mcp/troubleshooting): what each error means and how to fix it. - [uare.ai Experts MCP server](/docs/mcp/experts): the public server, with no key, through which assistants reach listed experts' AIs as visitors. --- # Tools reference > The nine tools your MCP server exposes, with their parameters and what each returns. These are the tools of [your MCP server](/docs/mcp/quickstart). Every one runs on your own AI, with your private context. Each one is marked with the behavior hints MCP clients use to decide when to ask before calling it. The [uare.ai Experts MCP server](/docs/mcp/experts) has three tools of its own, for visitors: see [its tools](/docs/mcp/experts#tools). | Tool | Does | Behavior | | --- | --- | --- | | [`generate_post`](#generate_post) | Drafts a social post in your voice | Drafts text | | [`respond_as_me`](#respond_as_me) | Drafts a reply in your voice | Drafts text | | [`decide`](#decide) | Applies your decision framework | Drafts text | | [`refuse`](#refuse) | Checks whether your AI would decline a request | Read-only | | [`act`](#act) | Works through your connected apps | Reads now, stages writes | | [`confirm_action`](#confirm_action) | Carries out a staged write | Writes | | [`learn`](#learn) | Queues a fact for your AI's knowledge | Queues for review | | [`list_skills`](#list_skills) | Lists your skills and preferences | Read-only | | [`get_skill`](#get_skill) | Reads one skill in full | Read-only | ## generate_post Composes a social media post in your voice. Your AI draws on its knowledge and your voice profile and returns the finished post. It doesn't publish anything. | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `topic` | string | Yes | What the post is about. | | `platform` | string | No | Target platform, for example `linkedin`, `twitter` or `instagram`. Defaults to `linkedin`. | | `length` | string | No | `short` (about 100 words), `medium` (about 200) or `long` (about 400). Defaults to `medium`. | | `anchors` | string[] | No | Personal experiences or themes to weave in. | Returns `post`, `platform` and `word_count`. ## respond_as_me Drafts a reply to a message in your voice, using what your AI knows about you and the people you work with. | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `message` | string | Yes | The message to reply to. | | `channel` | string | No | Where the reply goes, for example `email`, `slack` or `text`. Defaults to `email`. | | `intent` | string | No | What the reply should achieve. | Returns `response` and `channel`. ## decide Weighs options against your values, goals and past decisions, and returns a decision with its reasoning. | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `situation` | string | Yes | The decision to make. | | `options` | string[] | Yes | The options to choose from. | Returns `decision`. ## refuse Checks, without doing anything, whether a request fits your values. Agents can call it before an action tool. | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `request` | string | Yes | What's being asked. | Returns `would_refuse` (a boolean) and `reason`. ## act Asks your AI to do something through the apps you've connected in uare.ai, such as Gmail or Google Drive. Your AI picks the app and the action itself. - **Reads** (search, fetch, list) run right away. - **Writes** (send, create, delete) don't run here. Your AI returns a plan in `pending_actions`, with a `confirmation_token` you pass to [`confirm_action`](#confirm_action). | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `intent` | string | Yes | What you want done, for example "check my latest emails from my accountant". | Returns `response`, plus `pending_actions` when a write is waiting for confirmation. ## confirm_action Carries out a write that [`act`](#act) staged. A token works once and expires about 10 minutes after `act` returned it. | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `confirmation_token` | string | Yes | The token from `act`'s `pending_actions`. | Returns `executed`, `actions` and `result`. > [!IMPORTANT] > This is the only tool that changes anything outside uare.ai. MCP clients mark it as destructive, so most will ask you before calling it. ## learn Queues a new fact about you for your AI's knowledge. The fact is reviewed before it becomes part of what your AI knows. | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `content` | string | Yes | The fact, in plain language. Up to 2,000 characters. | | `dimension` | string | Yes | One of `identity`, `world`, `story`, `mindset`, `drive`, `pattern` or `growth`. | | `confidence` | number | No | How sure you are, from `0` to `1`. Defaults to `0.7`. | Returns `pending_fact_id` and `status`: `pending_approval` when the fact is waiting for review, or `queued_locally` when uare.ai couldn't queue it yet and retries in the background. ## list_skills Lists your skills and preferences: the markdown files you write to guide your AI. No parameters. Returns a list of entries with `name`, `type` (`skill` or `preference`), a short `preview` and `size` in bytes. An entry with `hidden: true` is a learned skill your AI doesn't use: its preview reads `[hidden]`, and `get_skill` won't return it. ## get_skill Returns one skill or preference in full, including its YAML frontmatter. | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `name` | string | Yes | The name from `list_skills`. | Returns `name`, `content` and `type` (`skill` or `preference`). Preferences are rules your AI always applies to itself; reading one tells an agent what your AI is working under. --- # API keys > Create, store and revoke the personal API keys that let MCP clients use your AI through your MCP server. A personal API key lets a client act as your AI, with your private context. Send it on every request: ```http Authorization: Bearer uare_pk_... ``` Keys start with `uare_pk_` followed by 32 hexadecimal characters. Today they work with [your MCP server](/docs/mcp/quickstart). The [uare.ai Experts MCP server](/docs/mcp/experts) takes no key. ## Create a key Keys live in your own Settings on uare.ai, so you, the AI's owner, create and revoke them. 1. Open [Settings → API keys](https://app.uare.ai/chat-with-me?settings=api-keys) in uare.ai. 2. Select **Generate new key** and give it a name you'll recognize later, such as the client or the machine it's for. 3. Copy the key. It's shown only once. ## Store it safely - Create one key per client or machine, so you can revoke one without breaking the others. - Keep keys in an environment variable or your client's secret storage, not in files you commit. - A key carries your private context. Don't share it, and don't put it in code that runs in a browser. - Never use a key to answer other people, such as your website's visitors: your AI would answer them with your private context. Visitors get the [website widget](/docs/website/quickstart), the [public API](/docs/api/build-your-own) or, in assistants, the [uare.ai Experts MCP server](/docs/mcp/experts), which need no key and run in public mode. ## Revoke a key In the same **API keys** list, revoke any key you no longer use or think was exposed. The next request with that key gets `403` and `API key has been revoked`. Generate a new key and update your client. ## What a key can do Every key can call every [tool of your MCP server](/docs/mcp/tools) for your AI. Keys have no scopes or expiry date yet, so revoking is how you limit one. --- # Troubleshooting > What each error from your MCP server means, and how to get your client connected again. This page is about [your MCP server](/docs/mcp/quickstart). For the uare.ai Experts MCP server, see [its errors](/docs/mcp/experts#errors). ## Errors from the server | Status | Message | What to do | | --- | --- | --- | | `401` | `Authorization: Bearer uare_pk_... is required` | Your client isn't sending the key. Check the `Authorization` header in its config. In a terminal, an empty `$UARE_API_KEY` sends `Bearer` with nothing after it and gets this error: `export` the key first. | | `401` | `Invalid MCP API key format` | The value after `Bearer ` doesn't start with `uare_pk_`. Look for a missing or extra character. If it reads `Bearer undefined`, your code built the header from an environment variable that isn't set where it runs, such as `process.env.UARE_API_KEY`. | | `401` | `Invalid MCP API key` | The key doesn't exist. Copy it again or [generate a new one](/docs/mcp/api-keys#create-a-key). | | `403` | `API key has been revoked` | Generate a new key and update your client. | | `429` | `Too many requests` | You went over the limit of 120 requests a minute. Wait a minute and retry. | | `503` | `Authentication service unavailable` | A temporary problem on our side. Retry after a few seconds. | ## Check your key from a terminal Send the first message every MCP client sends: ```bash curl -s https://app.uare.ai/api/v1/mcp \ -H "Authorization: Bearer $UARE_API_KEY" \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"curl","version":"1.0"}}}' ``` A working key answers with an `event: message` line whose data holds the server's `serverInfo`. A refused one answers with one of the errors above. ## Claude Desktop doesn't start the server Claude Desktop starts `npx` with a minimal `PATH`, so it often can't find Node.js installed through Homebrew, nvm or fnm. Replace `"command": "npx"` with the full path that `which npx` prints in your terminal, then restart Claude Desktop. Claude Desktop writes MCP logs to `~/Library/Logs/Claude/mcp*.log` on macOS. On Windows, a key that works elsewhere can fail because Claude Desktop splits the `Authorization` argument at its spaces. Use the config in [Claude Desktop on Windows](/docs/mcp/quickstart#claude-desktop-on-windows). ## The key works in a terminal but not in Cursor Cursor resolves `${env:UARE_API_KEY}` from the environment it was started in. If you open Cursor from the Dock, it doesn't see variables exported in your shell profile. Start Cursor from a terminal, or put the key directly in `mcp.json` and keep that file private. ## A change through your apps never happens [`act`](/docs/mcp/tools#act) only stages changes such as sending an email. Your client has to call [`confirm_action`](/docs/mcp/tools#confirm_action) with the token from `pending_actions` within about 10 minutes. If the token expired, ask again. ## `act` can't find your data `act` only reaches apps you've connected to your AI in uare.ai. Connect the app there first, then try again. --- # uare.ai Experts MCP server > A public MCP server with no key: an assistant such as ChatGPT or Claude finds a listed uare.ai expert for a person's need, and the person talks to that expert's AI as a visitor. The uare.ai Experts MCP server brings experts' AIs into the assistants people already use. An assistant such as ChatGPT or Claude finds a listed expert for what a person needs, the person talks to that expert's AI as a visitor without leaving their conversation, and they can continue with the expert on uare.ai. ```text https://app.uare.ai/api/v1/public/mcp ``` It's a public, remote MCP server over the streamable HTTP transport. It needs no API key and no sign-in, and it reaches only what any visitor can see: listed experts' public profiles, and the answers their AIs give in public mode. > [!NOTE] > This isn't [your MCP server](/docs/mcp/quickstart), which takes your API key, acts as you and reaches your private context. [Two MCP servers](/docs/concepts#two-mcp-servers) compares them. ## How it works 1. A person asks their assistant: "I'm running my first marathon in April. Who could help me plan my training?" 2. The assistant calls [`find_experts`](#find_experts) with that need. uare.ai searches the experts it lists and returns up to three, each with a one-line reason, or says that none matches. 3. The person picks Ana, a running coach. The assistant calls [`ask_expert`](#ask_expert) with their message, and Ana's AI answers as it would a visitor on her public profile. The reply is labelled as her AI's: "The AI of Ana replied". 4. With the next message, the assistant passes back the `consult` value from that reply, so Ana's AI remembers the conversation. See [Keep a conversation going](#keep-a-conversation-going). 5. [`expert_profile`](#expert_profile) shows more about Ana: her bio, her public services and a link to her page. When the conversation's free messages run out, the reply sends the person to that page. ## What experts should know - **You choose to be listed.** Only listed experts appear: `find_experts` searches only them, and `ask_expert` and `expert_profile` answer only for them. For now, uare.ai lists experts who agree to it. Once the uare.ai Experts app is in ChatGPT's and Claude's directories, a setting in your AI's settings (Settings → ChatGPT & Claude) lets you list your AI yourself, and take it down at once. Only a personal AI can be listed, not an organization's, and places are limited for now. uare.ai can take a listing down and keep it down. - **Your AI must be public.** A listed expert appears only while their AI is public. Make your AI private and it stops appearing and answering there at once. - **You must be verified and 18 or older on record.** The same rule uare.ai's own search uses: a listed expert appears only while their identity is verified and their account shows them as 18 or older, from the birthday on their profile or the date of birth their identity check confirmed. Without an age on record they don't appear, even when listed. - **It answers as on your public profile.** Your AI answers in public mode, from the knowledge you've made public, without your private notes or connected apps. Replies are labelled as your AI's, never as you in person. - **You see the conversations.** Conversations through these assistants are visitor conversations: they show with your AI's other guest conversations, beside chats from your public profile and your website. Like any guest, the person is anonymous. - **Prices stay on uare.ai.** `expert_profile` lists your public services by name, category and description, never with a price. In other assistants it can add whether a service is free or for subscribers; inside ChatGPT it leaves that out too. People see your prices on your page. - **People continue on your page.** Results link to your public profile. When a conversation's free messages run out, or a daily limit is reached, the reply sends the person there to keep talking to your AI. ## Connect it for testing To try the server, add it to an assistant yourself, as a custom MCP server with no sign-in. Assistants may ask before each `ask_expert` call, because it sends the person's message to uare.ai. `find_experts` and `expert_profile` are marked read-only. ### Claude In Claude on the web, Claude Desktop or Cowork, open **Customize → Connectors**, select **+ Add**, then **Add custom connector**. Name it, for example *uare.ai Experts*, paste the URL above, choose **No sign in**, then select **Add**. Turn it on in a conversation from the **+** menu, under **Connectors**. On Team and Enterprise plans, an Owner adds the connector first, in **Organization settings → Connectors**. Anthropic's guide, [Get started with custom connectors using remote MCP](https://support.claude.com/en/articles/11175166-get-started-with-custom-connectors-using-remote-mcp), has the current steps. In Claude Code, one command adds it: ```bash claude mcp add --transport http uare-experts \ https://app.uare.ai/api/v1/public/mcp ``` ### ChatGPT In ChatGPT on the web, open **Plugins**, select the plus button, then **Add custom MCP server**. Enter a name, paste the URL above as the **Server URL**, choose **No authentication**, accept the risk warning and select **Create as a plugin**. Install the plugin from your personal plugins, then type `@` in a conversation and pick it. Workspace settings can restrict custom MCP servers. OpenAI's guide, [Add custom MCP server](https://developers.openai.com/api/docs/guides/custom-mcp-server), has the current steps. ### Your own code Any MCP client can use the server. In Node.js, with the official SDK: ```bash npm install @modelcontextprotocol/sdk ``` ```js title="ask-an-expert.mjs" import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; const client = new Client({ name: "my-assistant", version: "1.0.0" }); await client.connect( new StreamableHTTPClientTransport(new URL("https://app.uare.ai/api/v1/public/mcp")), ); const found = await client.callTool({ name: "find_experts", arguments: { need: "I am running my first marathon in April and need a training plan" }, }); const [expert] = found.structuredContent?.experts ?? []; if (!expert) { // No listed expert is close enough: the text says so. console.log(found.content[0].text); } else { const ask = (message, consult) => client.callTool({ name: "ask_expert", arguments: { expert_id: expert.expert_id, message, ...(consult && { consult }) }, }); const first = await ask("I run 20 km a week. Can I be ready for a marathon in April?"); console.log(first.content[0].text); // Pass the consult back to stay in the same conversation. const second = await ask("What should my long runs look like?", first.structuredContent?.consult); console.log(second.content[0].text); } await client.close(); ``` The server keeps no session, so a single request can call a tool, with no `initialize` first: ```bash curl -s https://app.uare.ai/api/v1/public/mcp \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"find_experts","arguments":{"need":"I am running my first marathon in April and need a training plan"}}}' ``` It answers with an `event: message` line whose data holds the JSON-RPC result. ## Tools | Tool | Does | Behavior | | --- | --- | --- | | [`find_experts`](#find_experts) | Finds up to three listed experts for a need, each with a one-line reason | Read-only | | [`ask_expert`](#ask_expert) | Sends a message to an expert's AI and returns its reply | Sends the message to uare.ai | | [`expert_profile`](#expert_profile) | Returns an expert's profile, their services and a link to their page | Read-only | Each result has a text block written for the assistant's model, and the same data as JSON in `structuredContent` for your own code. An error is a result with `isError: true` and a message to show the person: see [Errors](#errors). ## find_experts Finds up to three listed experts whose AI can help with a need, best match first. uare.ai compares the need with each listed expert's public profile and keeps only close matches. When none is close enough, `experts` is empty and the text says `No uare.ai expert matches this need yet.` | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `need` | string | Yes | What the person needs help with, in their words. Up to 1,000 characters. | ```json { "experts": [ { "expert_id": "08aeb20d-b364-4dd4-a36e-7c5c55887015", "name": "Ana", "reason": "Story: Ana has coached first-time marathoners for ten years.", "profile_url": "https://app.uare.ai/p/08aeb20d-b364-4dd4-a36e-7c5c55887015?utm_source=claude&utm_medium=assistant&utm_campaign=uare_experts_app" } ] } ``` | Field | What it is | | --- | --- | | `expert_id` | The expert's ID, for `ask_expert` and `expert_profile`. | | `name` | The expert's name. | | `reason` | Why they match: one line from the part of their public profile closest to the need. | | `profile_url` | Their page on uare.ai. `utm_source` names the assistant: `claude` or `chatgpt` when uare.ai recognizes that assistant's servers, `mcp` otherwise. | ## ask_expert Sends a message to an expert's AI and returns its reply. The expert's AI answers in public mode, as it would a visitor on the expert's public profile. | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `expert_id` | string | Yes | The `expert_id` from `find_experts`. | | `message` | string | Yes | The message for the expert, in the person's words. Up to 4,000 characters. | | `consult` | string | No | The `consult` value from the previous `ask_expert` reply. Leave it out to start a new conversation. | The text block gives the model the reply, then the `consult` value with an instruction to pass it back: ```text The AI of Ana replied: Yes, if you build up gradually. How many times a week do you run now? consult: eyJhbGciOiJIUzI1NiJ9… Pass this consult value with the next ask_expert call to continue this conversation (4 free messages left). More from Ana: https://app.uare.ai/p/08aeb20d-b364-4dd4-a36e-7c5c55887015?utm_source=claude&utm_medium=assistant&utm_campaign=uare_experts_app ``` `structuredContent` holds the same: ```json { "expert": { "expert_id": "08aeb20d-b364-4dd4-a36e-7c5c55887015", "name": "Ana", "avatar_url": "https://…/ana.png" }, "reply": "Yes, if you build up gradually. How many times a week do you run now?", "consult": "eyJhbGciOiJIUzI1NiJ9…", "new_conversation": false, "remaining_free_messages": 4, "suggestions": [ { "label": "Plan my long runs", "prompt": "What should my long runs look like?" } ], "profile_url": "https://app.uare.ai/p/08aeb20d-b364-4dd4-a36e-7c5c55887015?utm_source=claude&utm_medium=assistant&utm_campaign=uare_experts_app" } ``` | Field | What it is | | --- | --- | | `expert` | The `expert_id`, `name` and `avatar_url` of the expert whose AI replied. | | `reply` | The reply, often with markdown. | | `consult` | Pass it back with the next message to stay in this conversation. | | `new_conversation` | `true` when the `consult` you sent had expired, so this reply starts a new conversation. | | `remaining_free_messages` | Free messages left in this conversation. | | `suggestions` | Follow-ups the expert's AI offers: a `label` to show and a `prompt` to send. Can be empty. | | `profile_url` | The expert's page on uare.ai. | When a limit is reached, `ask_expert` returns a hand-off instead of a reply: see [Limits](#limits). ## expert_profile Returns an expert's uare.ai profile, the services they offer and a link to their page. | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `expert_id` | string | Yes | The `expert_id` from `find_experts`. | ```json { "expert": { "expert_id": "08aeb20d-b364-4dd4-a36e-7c5c55887015", "name": "Ana", "avatar_url": "https://…/ana.png", "bio": "Running coach. I help first-time marathoners train without getting hurt.", "profile_url": "https://app.uare.ai/p/08aeb20d-b364-4dd4-a36e-7c5c55887015?utm_source=claude&utm_medium=assistant&utm_campaign=uare_experts_app" }, "services": [ { "name": "Marathon plan review", "category": "education", "description": "I read your training plan and tell you what to change.", "access": "for subscribers" }, { "name": "Ask me about your first 10K", "access": "free" } ] } ``` `services` lists the expert's published public services, featured ones first. Each has a `name` and, when they're set, a `category` and a one-line `description`. `access` says `free` or `for subscribers` when the service is one of those. No price is ever included, and inside ChatGPT `access` is left out too. If the services can't be read, `services` is `null`, the text says so, and the rest of the profile still comes back. ## Keep a conversation going The server keeps no session between calls. The `consult` value is the conversation: `ask_expert` returns one with every reply, in `structuredContent` and on the `consult:` line of its text, and the assistant passes it back with the next message. Without it, `ask_expert` starts a new conversation. - **What the expert's AI remembers.** Everything said in one consult, as in a visitor's chat on uare.ai, and nothing from other consults. - **How long it lasts.** Up to two hours, and it ends sooner after an hour without messages. - **When it expires.** `ask_expert` doesn't fail. It starts a new conversation with the same expert and says so: its text opens with `Started a new conversation with the AI of Ana: the previous consult had expired.`, `new_conversation` is `true`, and it returns a new `consult`. That conversation counts as a new one in the [daily limits](#limits). - **One message at a time.** A message sent while the previous reply is still being written gets an error. Wait for the reply, then send. - **It belongs to one person and one expert.** Keep it with that person's conversation, and treat it as opaque. A value that was changed, a session token from the [public chat API](/docs/api/build-your-own), or another expert's consult gets an error that says why. It never quietly starts a new conversation. ## Limits Conversations through the server are free, so it keeps to limits. They count per caller: one person, when the assistant identifies its user in a way uare.ai can verify, and otherwise one network address. Servers uare.ai recognizes as an assistant's, such as Anthropic's for Claude, count as one shared address with a larger allowance, because all of that assistant's users come through them. Daily limits reset at midnight UTC. - **Free messages per conversation.** A consult includes the same free messages as a [visitor's conversation](/docs/concepts#visitor-conversations) on a website. Each reply says how many are left. - **New conversations per expert.** A caller can start a limited number of new conversations with the same expert each day. - **Messages per day.** Each caller has a daily share of messages, within a daily budget for the whole server. - **Requests per minute.** Each caller can send only so many requests a minute, `initialize` and `tools/list` included. Past that, the server answers `429` with a `Retry-After` header. Only `ask_expert` counts toward the daily limits, so `find_experts` and `expert_profile` keep working. When the free messages or a daily limit stop a message, `ask_expert` doesn't fail: it hands the person over to the expert's page. The result isn't an error, and `structuredContent.handoff` says why, with the link: | `handoff.reason` | When | The text starts with | | --- | --- | --- | | `message_limit` | The conversation's free messages are used. | `This conversation with the AI of Ana has used its free messages.` | | `consult_limit` | The caller has started its share of new conversations with this expert today. | `The free conversations with the AI of Ana through this assistant are used up for today.` | | `budget` | The caller's daily share, or the server's daily budget, is used. | `The AI of Ana can't take more conversations through this assistant right now.` | Each text ends with `Continue with Ana on uare.ai:` and the expert's page, which is also `handoff.url`. Show the person that link: on uare.ai they can keep talking to the expert's AI and sign up. If uare.ai can't count requests or messages for a moment, it refuses them rather than letting them through uncounted. ## Errors A tool that fails returns a result with `isError: true` and a message the assistant can show the person: | Tool | Message | What it means | | --- | --- | --- | | `ask_expert`, `expert_profile` | `This expert isn't available.` | The `expert_id` isn't a listed expert's, their AI is no longer public, or they aren't verified and 18 or older on record. | | `find_experts` | `Describe the need in a few words.` | The `need` was blank. | | `ask_expert` | `Send a message for the expert.` | The `message` was blank. | | `ask_expert` | `Couldn't continue that consult: the consult value isn't valid. …` | The value was changed or isn't one uare.ai issued. Call `ask_expert` without `consult` to start a new conversation. | | `ask_expert` | `Couldn't continue that consult: it wasn't started through this app. …` | It's a session token from the public chat API, not a consult. | | `ask_expert` | `That consult belongs to expert …` | The consult is another expert's. Use it with that expert, or start a new conversation with this one. | | `ask_expert` | `The AI of Ana is still answering the previous message in this consult. …` | Wait for that reply, then send the next message. | | `ask_expert` | `The AI of Ana couldn't answer just now. Try again in a moment.` | The reply failed or came back empty. When the error carries a `consult` value, retry with it to stay in the conversation. | | `ask_expert` | `The AI of Ana can't take messages here right now. Try again later.` | uare.ai can't count messages right now, so it takes none. | | `ask_expert` | `The expert can't be reached right now. …` | uare.ai couldn't check the expert's listing. Retry in a moment. | | `expert_profile` | `The expert's profile can't be read right now. …` | uare.ai couldn't check the expert's listing. Retry in a moment. | | `find_experts` | `Expert search is unavailable right now. Try again in a moment.` | `find_experts` couldn't search. | | Any | `Input validation error: Invalid arguments for tool …` | An argument doesn't fit the tool's parameters, and the rest of the message names it: a `message` over 4,000 characters (`Too big: expected string to have <=4000 characters`), a `need` over 1,000, an empty or missing argument, or an `expert_id` that isn't an ID in the form `find_experts` returns. | | Any | `Something went wrong on uare.ai's side. Try again in a moment.` | An unexpected failure. | The HTTP request can also be refused before any tool runs: | Status | What it means | | --- | --- | | `429` | Too many requests from this caller. Wait the seconds in `Retry-After`, then retry. | | `503` | uare.ai can't count requests right now. Retry after `Retry-After`. | | `405` | The server answers `POST` only. It keeps no session, so it doesn't serve `GET` or `DELETE`. | | `411` | A `DELETE` without a `Content-Length` header is refused before it reaches the server, with `Content-Length header required for this route`. Sent with `Content-Length: 0`, it gets the `405` above. | --- # Changelog > New integrations and changes to the uare.ai developer platform, newest first. ## October 2026 ### 2026-10-23: A setting to list your AI in ChatGPT and Claude - **A setting to be listed, coming with the directories.** Once the uare.ai Experts app is in ChatGPT's and Claude's directories, **Available in ChatGPT and Claude** in your AI's settings (Settings → ChatGPT & Claude) lets the [uare.ai Experts MCP server](/docs/mcp/experts) find your AI and let people talk to it. It says what stands in the way when your AI can't be listed yet: a private profile, an identity not yet verified, or no age on record. Until then, uare.ai lists experts by invitation. ### 2026-10-22: Website widget fixes, and docs from an outside developer's test - **`uare:ready` fires once per load.** A `` element in your page's HTML loaded your AI twice and fired `uare:ready` twice. It now loads once, and fires again only if you change `handle` or `api-base`. - **`send()` says when it doesn't send.** Before your AI has loaded, while a reply is streaming, with empty text or after the free messages, it used to resolve and drop the message. It now rejects with an `Error` whose `code` says why, and it rejects when the reply fails too. See [JavaScript](/docs/website/customize#javascript). - **`open` works in React 19.** React 19 sets `` as a property, which replaced the `open()` method. `open` now takes a boolean as a property and still gives the method when read. The [type declaration](/docs/website/customize#react-and-nextjs) types the element. - **A smaller avatar.** The widget loads a small version of your AI's picture instead of the full-size one. - **Docs.** The JavaScript example works with the one-line snippet. The public API examples start a session with the visitor's first message. Documented `403 origin_required` for server-side code, regenerated replies using up a free message, the [`RateLimit-*` headers](/docs/api/reference#rate-limit-headers), the Experts server's validation errors and `411`, and [Claude Desktop on Windows](/docs/mcp/quickstart#claude-desktop-on-windows). The Experts example handles a need nobody matches, and [`learn`](/docs/mcp/tools#learn) lists what it returns. The pages say the AI's owner allows websites and creates API keys. ### 2026-10-21: uare.ai Experts MCP server - **A public MCP server for listed experts.** `https://app.uare.ai/api/v1/public/mcp` needs no key. An assistant such as ChatGPT or Claude finds a listed expert for a person's need with `find_experts`, the person talks to that expert's AI as a visitor with `ask_expert`, and `expert_profile` shows the expert's public profile and services. A `consult` value carries each conversation from one message to the next. Listing is by invitation for now. See [uare.ai Experts MCP server](/docs/mcp/experts). - **Two MCP servers, told apart.** The MCP pages and [Concepts](/docs/concepts#two-mcp-servers) now separate [your MCP server](/docs/mcp/quickstart), which takes your API key and acts as you, from the Experts server, which takes no key and serves visitors only. ### 2026-10-18: Your MCP server from your own code - Added [Use the tools from your own code](/docs/mcp/quickstart#use-the-tools-from-your-own-code) and [Check your key from a terminal](/docs/mcp/troubleshooting#check-your-key-from-a-terminal). The Claude Code command now keeps your key out of Claude Code's config. ### 2026-10-17: Iframe embeds - Added [Embed with an iframe](/docs/website/customize#embed-with-an-iframe) for site builders that only accept iframes, and a complete page for the [plain JavaScript](/docs/api/build-your-own#plain-javascript) chat. ### 2026-10-16: Your AI on your website - **Website widget.** One line of HTML puts a chat with your AI on your site, as a bubble or inside your page. Visitors talk to it in public mode. See [Add your AI to your website](/docs/website/quickstart) and [Customize the website widget](/docs/website/customize). - **Public chat API.** Build your own chat UI: start a session, then stream replies in the AI SDK UI message format that `useChat` reads directly. See [Build your own chat UI](/docs/api/build-your-own) and the [reference](/docs/api/reference). - **Allowed websites.** Choose which sites may show your AI in Settings → Website. The list starts empty. - API keys moved to Settings → API keys. The [MCP pages](/docs/mcp/api-keys) show the new steps. ### 2026-10-16: Developer docs - Launched these docs. Every page has a markdown version: add `.md` to its URL, or send `Accept: text/markdown`. - Published [/llms.txt](/llms.txt) and [/llms-full.txt](/llms-full.txt) for AI agents. - Documented your MCP server: the [quickstart](/docs/mcp/quickstart), all nine [tools](/docs/mcp/tools), [API keys](/docs/mcp/api-keys) and [troubleshooting](/docs/mcp/troubleshooting).