Jarvis Embed SDK¶
Embed the Jarvis AI Assistant in any web application. Works with bundlers and script tags.
Installation¶
Browser (no bundler)¶
A pre-built UMD bundle is served via GitHub Pages. Load the latest version directly with a <script> tag and access the class via window.JarvisSDK:
<script src="https://ascending-llc.github.io/jarvis-embed/latest/jarvis-embed.js"></script>
<script>
const { JarvisEmbed } = window.JarvisSDK;
const jarvis = new JarvisEmbed({
provider: 'google',
token: googleIdToken,
apiUrl: 'https://jarvis.host.com',
containerId: 'chat-container',
});
</script>
To pin to a specific version, replace latest with the version number:
The bundle is also included in the npm package at dist/index.global.js if you prefer to self-host it.
Usage¶
import { JarvisEmbed } from '@ascending-inc/jarvis-embed';
const jarvis = new JarvisEmbed({
provider: 'google',
token: googleIdToken,
containerId: 'chat-container',
apiUrl: 'https://jarvis.host.com',
spec: 'my-spec',
agentId: 'agent_123',
artifactsButton: false,
onReady: (jarvisToken) => jarvis.setMcpServers(['my-mcp-server']),
});
Options¶
| Option | Type | Default | Description |
|---|---|---|---|
provider |
AuthProvider |
Required | Auth provider — see Authentication. |
apiUrl |
string |
Required | Jarvis API endpoint (e.g. https://jarvis.host.com). |
token |
string |
Required (except hmac) |
OAuth / JWT token. Omit for hmac. |
containerId |
string |
— | ID of the DOM element to mount the iframe into. |
container |
HTMLElement |
— | Direct element reference (alternative to containerId). |
width |
string |
'100%' |
CSS width of the iframe. |
height |
string |
'600px' |
CSS height of the iframe. |
iframeUrl |
string |
{apiUrl}/v1/chat |
Override just the embedded chat page URL. Useful for local iframe testing while keeping auth/API calls pointed at apiUrl. |
spec |
string |
— | Spec identifier to use for the conversation (sent as ?spec= to the API). Retrieve available values from GET {apiUrl}/api/config. |
agentId |
string |
— | Agent identifier to use for the conversation (sent as ?agent_id= to the embedded chat). |
artifactsButton |
boolean |
false |
Initial visibility state of the artifacts button in the embedded chat UI. |
debug |
boolean |
false |
Log SDK activity to the console. |
onReady |
(jarvisToken: string) => void |
— | Fires when the iframe is authenticated and ready. Receives the Jarvis session token — use it to call Jarvis APIs (e.g. GET {apiUrl}/api/mcp/servers) on behalf of the user. |
onError |
(err: Error) => void |
— | Fires on failure. |
onMessage |
(data: unknown) => void |
— | Fires when the iframe posts a message to the host page. |
If neither containerId nor container is provided the iframe appends to document.body.
Local iframe testing¶
If you want to use production auth/API endpoints but load the chat UI from a local dev server, set iframeUrl separately:
new JarvisEmbed({
provider: 'google',
token: googleIdToken,
containerId: 'chat-container',
apiUrl: 'https://jarvis.host.com',
iframeUrl: 'http://localhost:3090/c/new',
});
When iframeUrl is provided, the SDK will:
- keep token exchange and API calls on
apiUrl - load the iframe from
iframeUrl - use the
iframeUrlorigin forpostMessage
Getting a spec¶
Available specs can be retrieved from the Jarvis config endpoint:
Using an agent¶
To start the embedded chat in agent mode, pass agentId in the constructor config:
new JarvisEmbed({
provider: 'google',
token: googleIdToken,
containerId: 'chat-container',
apiUrl: 'https://jarvis.host.com',
agentId: 'agent_123',
});
Authentication¶
For google, s_jwt, a_jwt, and hmac, the SDK sends your auth payload to POST {apiUrl}/api/auth/exchange, and Jarvis returns a short-lived session token for the embedded chat. With direct, the SDK skips the exchange call and uses your JWT as-is.
google¶
Pass the Google id_token you receive from OAuth2 directly — no server-side token signing is required.
| Provider | Token |
|---|---|
google |
Google id_token from OAuth2 |
s_jwt / a_jwt¶
| Provider | Token |
|---|---|
s_jwt |
JWT signed with a shared secret (HS256) |
a_jwt |
JWT signed with a private key (RS256 / ES256) |
Generating the token on your server¶
Your server signs a JWT that Jarvis verifies. The two providers differ only in the signing algorithm and key material:
| Provider | Algorithm | Signing key | Jarvis verifies with |
|---|---|---|---|
s_jwt |
HS256 |
A shared secret (CUSTOM_JWT_SECRET) |
The same shared secret |
a_jwt |
RS256 (or ES256) |
Your RSA/EC private key (CUSTOM_JWT_PRIVATE_KEY) |
The matching public key |
The iss, aud, and (for a_jwt) kid values must match the configured values in the Jarvis deployment.
Required claims
These are the claims Jarvis validates on every token.
| Claim | Type | Description |
|---|---|---|
sub |
string |
Username — a user's login ID. Must be a stable, unique identifier (e.g. username@domain.com) |
iss |
string |
Issuer — the base URL of the Jarvis deployment. |
aud |
string |
Audience — (e.g. "jarvis-services"). |
iat |
number |
Issued-at Unix timestamp (seconds). |
exp |
number |
Expiry Unix timestamp (seconds). Maximum 24 hours from iat. |
JWT header
| Field | Value |
|---|---|
alg |
HS256 for s_jwt, RS256 / ES256 for a_jwt |
kid |
Key ID (a_jwt only) |
Node.js example
The payload is identical for both providers — only the signing options change. Swap the commented block to switch providers:
import jwt from 'jsonwebtoken';
const JARVIS_ISS = 'https://jarvis.host.com';
const JARVIS_AUD = 'jarvis-services';
function generateJarvisToken(username, { expiresInHours = 1 } = {}) {
const now = Math.floor(Date.now() / 1000);
const payload = {
sub: username,
iss: JARVIS_ISS,
aud: JARVIS_AUD,
iat: now,
exp: now + expiresInHours * 3600,
};
// a_jwt — sign with an RSA/EC private key
return jwt.sign(payload, process.env.CUSTOM_JWT_PRIVATE_KEY, {
algorithm: 'RS256',
keyid: 'self-signed-key-v1',
});
// s_jwt — sign with a shared secret
// return jwt.sign(payload, process.env.CUSTOM_JWT_SECRET, {
// algorithm: 'HS256',
// });
}
Environment variables¶
Your server
| Variable | Description |
|---|---|
CUSTOM_JWT_PRIVATE_KEY |
PEM-encoded RSA/EC private key used to sign a_jwt tokens. |
CUSTOM_JWT_SECRET |
Shared secret used to sign s_jwt tokens. |
The iss, aud, and kid values are deployment-specific constants that can be hardcoded or stored as environment variables. They must match exactly what Jarvis is configured to expect.
Jarvis
| Variable | Description |
|---|---|
CUSTOM_JWT_PUBLIC_KEY |
PEM-encoded RSA/EC public key. Jarvis uses this to verify a_jwt tokens signed by your server. |
CUSTOM_JWT_SECRET |
Shared secret. Jarvis uses this to verify s_jwt tokens. |
CUSTOM_JWT_ISSUER |
Expected issuer claim value (e.g., "https://jarvis.host.com"). |
CUSTOM_JWT_AUDIENCE |
Expected audience claim value (e.g., "jarvis-services"). |
ALLOW_EMBED |
Set to true to enable the embed token flow (/api/auth/exchange and direct tokens). |
direct¶
With direct, your server mints the Jarvis-trusted JWT itself and the SDK uses it as-is — the /api/auth/exchange round-trip is skipped.
new JarvisEmbed({
provider: 'direct',
token: jarvisToken, // generated by your server
apiUrl: 'https://jarvis.host.com',
containerId: 'chat-container',
});
How the token is authenticated¶
The direct token is delivered to the embedded chat via SDK_AUTH and presented as a Bearer token on every request to Jarvis. Jarvis validates it through its Passport strategies, choosing one per request:
| Condition | Strategy |
|---|---|
OPENID_REUSE_TOKENS=true and the request carries a token_provider=openid cookie with a valid signed session |
openidJwt |
| Default | jwt |
The openidJwt strategy loads the federated tokens from the server-side session, which is resolved using the Session ID cookie the browser sends with each request. If that cookie is missing, expired, or the session store has been cleared, req.session cannot be hydrated and Jarvis will fall back to the standard jwt strategy or return a 401 error code.
hmac¶
new JarvisEmbed({
provider: 'hmac',
userId: 'user_123',
timestamp: Math.floor(Date.now() / 1000),
signature: hmacHex, // HMAC-SHA256(userId + timestamp)
containerId: 'chat-container',
apiUrl: 'https://jarvis.host.com',
});
Requests older than 5 minutes are rejected server-side.
Methods¶
| Method | Signature | Description |
|---|---|---|
destroy |
() => void |
Removes the iframe and cleans up the window message listener. Call this on unmount — essential for React. |
setMcpServers |
(servers: string[]) => void |
Activates one or more MCP servers by name. Safe to call before the iframe is ready — servers are queued and flushed automatically on SDK_READY. |
setArtifactsButton |
(enabled: boolean) => void |
Shows or hides the artifacts button at runtime. Safe to call before the iframe is ready — the latest value is queued and applied automatically on SDK_READY. |
setAgentId |
(agentId: string) => void |
Switches the embedded chat to a specific agent at runtime. Safe to call before the iframe is ready — the latest value is queued and applied automatically on SDK_READY. Empty or whitespace-only values are ignored. |
MCP (Model Context Protocol)¶
Pass one or more MCP server names to give Jarvis access to external tools and data sources during a session.
Discovering available servers¶
Call GET {apiUrl}/api/mcp/servers with the Jarvis token as a Bearer header to retrieve the names of all servers available to the authenticated user. The response is an object keyed by server name:
const jarvis = new JarvisEmbed({
provider: 'google',
token: googleIdToken,
containerId: 'chat-container',
apiUrl: 'https://jarvis.host.com',
onReady: async (jarvisToken) => {
const res = await fetch(`${apiUrl}/api/mcp/servers`, {
headers: { Authorization: `Bearer ${jarvisToken}` },
});
const servers = await res.json(); // { "posthog": {...}, "github": {...}, ... }
const names = Object.keys(servers);
// Activate all of them, or let the user pick from `names`
jarvis.setMcpServers(names);
},
});
Activating servers¶
The safest place to call setMcpServers is inside onReady, which fires once the iframe has authenticated and is listening:
const jarvis = new JarvisEmbed({
provider: 'google',
token: googleIdToken,
containerId: 'chat-container',
apiUrl: 'https://jarvis.host.com',
onReady: () => {
jarvis.setMcpServers(['posthog', 'aws-knowledge']);
},
});
You can also call it at any time after instantiation — if the iframe isn't ready yet the servers are queued internally and sent as soon as SDK_READY fires:
const jarvis = new JarvisEmbed({
provider: 's_jwt',
token: myJwt,
containerId: 'chat-container',
apiUrl: 'https://jarvis.host.com',
});
// Called immediately — queued until SDK_READY
jarvis.setMcpServers(['github', 'jira']);
To swap the active server set later (e.g. after a user action), call setMcpServers again with the new list:
document.getElementById('enable-analytics')?.addEventListener('click', () => {
jarvis.setMcpServers(['posthog']);
});
setArtifactsButton(enabled: boolean)¶
Shows or hides the artifacts button at runtime. If called before the iframe is ready, the value is queued and applied once the SDK is ready.
setAgentId(agentId: string)¶
Switches the embedded chat to a specific agent at runtime. If called before the iframe is ready, the value is queued and applied once the SDK is ready.
This is useful when the host app lets the user choose an agent after the widget has already been mounted:
document.getElementById('agent-picker')?.addEventListener('change', (event) => {
const nextAgentId = (event.target as HTMLSelectElement).value;
jarvis.setAgentId(nextAgentId);
});
React¶
useJarvis is not exported from the package — copy examples/react/src/useJarvis.ts into your project. It wraps JarvisEmbed in a useEffect and calls destroy() on unmount automatically:
import { useEffect, useRef } from 'react';
import { JarvisEmbed } from '@ascending-inc/jarvis-embed';
import type { JarvisConfig } from '@ascending-inc/jarvis-embed';
export function useJarvis(config: JarvisConfig | null) {
const jarvisRef = useRef<JarvisEmbed | null>(null);
useEffect(() => {
if (!config) return;
jarvisRef.current = new JarvisEmbed(config);
return () => {
jarvisRef.current?.destroy();
jarvisRef.current = null;
};
}, [config]);
return jarvisRef;
}
Pass null to defer initialization until the user is authenticated. The hook calls destroy() automatically on unmount, so there are no memory leaks or stale event listeners.
Using the container prop in React¶
When mounting into a React-managed DOM node, use a callback ref so initialization only happens once the element actually exists. Wrap config in useMemo with the container as a dependency — this ensures the SDK sees a real HTMLElement, not null:
import { useCallback, useMemo, useState } from 'react';
import { useJarvis } from './useJarvis';
function ChatWidget({ googleToken }: { googleToken: string }) {
const [container, setContainer] = useState<HTMLDivElement | null>(null);
const config = useMemo(() => {
if (!container || !googleToken) return null;
return {
provider: 'google' as const,
token: googleToken,
container,
width: '100%',
height: '100%',
onReady: (jarvisToken: string) => {
// fetch available servers and activate them
},
};
}, [container, googleToken]);
const jarvisRef = useJarvis(config);
return <div ref={setContainer} style={{ flex: 1 }} />;
}
Using containerId instead avoids this entirely — the SDK does the getElementById lookup itself after the iframe loads — but the container prop approach above is required when the element is managed by React state.
Examples¶
Both examples demonstrate Google OAuth, MCP tool selection, and embedding the chat widget. They share the same Express backend for token exchange.
1. Clone and set up¶
setup installs root dependencies, builds the SDK, and installs dependencies for both examples.
2. Configure environment variables¶
Each example has its own .env. Copy and fill in both:
cp examples/vanilla/.env.example examples/vanilla/.env
cp examples/react/.env.example examples/react/.env
| Variable | Description |
|---|---|
GOOGLE_CLIENT_ID |
OAuth client ID from Google Cloud Console |
GOOGLE_CLIENT_SECRET |
OAuth client secret (never sent to the browser) |
REDIRECT_URI |
Must match what's registered in Google Cloud Console |
JARVIS_URL |
Required — Jarvis API endpoint (e.g. https://jarvis.host.com) |
JARVIS_SPEC |
Optional spec override |
PORT |
Express port (default 5500) |
3. Run an example¶
Vanilla JS — floating chat widget, served at http://localhost:5500
React — useJarvis hook demo with proper cleanup, served at http://localhost:5501