🚀 Socket Launch Week Day 5:Introducing Repository Access Permissions and Custom Roles.Learn more
Sign In

@useatlas/sdk

Package Overview
Dependencies
Maintainers
1
Versions
13
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@useatlas/sdk - npm Package Compare versions

Comparing version
0.0.12
to
0.0.14
+284
dist/mcp.d.ts
/**
* Programmatic MCP onboarding helpers — OAuth 2.1 + DCR + PKCE flow
* reshaped so it runs from a server-side framework, a browser, an
* embedded React component, or a Node CI script. The
* `@useatlas/mcp init --hosted` CLI binds a `127.0.0.1:0` loopback
* listener to receive the redirect; this module delegates that step
* to the caller — `beginConnect` returns the `authorizationUrl` for
* the caller to open (popup / redirect / new tab) and `completeConnect`
* exchanges the resulting `code` for a JWT.
*
* ── What the caller must persist between begin → complete ───────────
*
* `state`, `codeVerifier`, `clientId`, and `tokenEndpoint` are returned
* from `beginConnect` and required by `completeConnect`. In a browser
* popup flow the caller stores them in `sessionStorage`; in a server-
* side flow the caller stashes them in the user's session record.
*
* The hook in `@useatlas/react` wraps this lifecycle for the popup
* case so SDK consumers don't reimplement the bookkeeping.
*
* ── Tree-shaking ──────────────────────────────────────────────────
*
* This module imports nothing from `@modelcontextprotocol/sdk` — only
* the standard browser/node OAuth + crypto primitives via the shared
* `@atlas/oauth-helper`, which is bundled into the SDK at build time
* (it is never resolved from npm). The published `.d.ts` does not
* import or re-export the helper package — the JSDoc references it,
* but TypeScript doesn't resolve those. Anyone who never imports
* `mcp.ts` pays nothing.
*/
/**
* Error codes carried by `AtlasMcpError`. The first eight values are
* 1:1 with `@atlas/oauth-helper`'s `OAuthHelperErrorCode` (re-declared
* inline so the published `.d.ts` doesn't import the internal-only
* helper package). The next three (`callback_*`) are produced inside
* the SDK proper. The remaining popup codes (`popup_blocked`,
* `popup_closed`) are reserved for the React popup driver in
* `@useatlas/react`'s `use-mcp-connect`, which throws against this
* union; the SDK proper never throws those itself.
*/
export type AtlasMcpErrorCode = "invalid_api_url" | "invalid_token_endpoint" | "discovery_failed" | "registration_failed" | "token_exchange_failed" | "issuer_mismatch" | "malformed_jwt" | "missing_workspace_claim" | "callback_state_mismatch" | "callback_state_missing" | "callback_missing_code" | "popup_blocked" | "popup_closed" | "grant_not_supported" | "workspace_not_in_grant_list";
export declare class AtlasMcpError extends Error {
readonly code: AtlasMcpErrorCode;
constructor(message: string, code: AtlasMcpErrorCode, options?: ErrorOptions);
}
export interface BeginConnectOptions {
/**
* Atlas API base — e.g. `https://mcp.useatlas.dev`. Discovery doc lives
* at `${apiUrl}/.well-known/oauth-authorization-server/api/auth`. Must
* be `https://`, except for `http://127.0.0.1` / `http://localhost`
* which are accepted for local-dev testing.
*/
apiUrl: string;
/** Human-readable name registered via DCR — shown on the consent screen. */
clientName: string;
/**
* Where the auth server should send the user after consent. Must match
* the `redirect_uri` the caller listens on (e.g. a `/oauth/callback`
* page in a Next.js app or the popup-host page in a browser flow).
*/
redirectUri: string;
/** Defaults to `["mcp:read", "offline_access"]`. */
scopes?: ReadonlyArray<string>;
/**
* Forward-compat slot (#2196). **No-op today.** The Atlas OAuth
* provider does not yet accept a workspace hint on the authorize
* endpoint — server-side claim issuance reads the user's session at
* consent time and emits the singular + (optionally) plural claims
* based on membership, regardless of what you pass here. Reserved so
* the SDK surface doesn't need to change when the provider lands
* the hint.
*
* Passing this option today has no observable effect — to get a
* multi-workspace token, just ensure the authenticating user belongs
* to more than one workspace. The plural claim then surfaces on
* `completeConnect`'s result as `workspaces`.
*/
workspaceId?: string;
/** Test seam — defaults to global `fetch`. */
fetchImpl?: typeof fetch;
/** Test seam — defaults to `crypto.getRandomValues`. */
randomBytesImpl?: (length: number) => Uint8Array;
}
export interface BeginConnectResult {
/** URL the caller redirects/popup-opens for the user to consent. */
authorizationUrl: string;
/**
* Anti-CSRF state. Caller must echo this back to `completeConnect.expectedState`
* when the callback fires.
*/
state: string;
/**
* PKCE code verifier. Caller must persist between begin and complete
* (sessionStorage in a browser, session record on a server).
*/
codeVerifier: string;
/** DCR-issued client_id for this caller. Persist alongside `codeVerifier`. */
clientId: string;
/** Cached for `completeConnect` so it doesn't re-discover. */
tokenEndpoint: string;
/** Discovered issuer claim — used to verify the JWT's `iss`. */
issuer: string;
}
export interface CompleteConnectOptions {
apiUrl: string;
/** State value received in the callback (`?state=`). */
state: string;
/** State value originally returned from `beginConnect`. */
expectedState: string;
/** Authorization code received in the callback (`?code=`). */
code: string;
/** PKCE verifier from `beginConnect`. */
codeVerifier: string;
/** DCR-issued client_id from `beginConnect`. */
clientId: string;
/** Same `redirect_uri` you passed to `beginConnect`. */
redirectUri: string;
/**
* From `beginConnect.tokenEndpoint` — skips a re-discover roundtrip.
* Pass alongside `issuer`; passing only one re-discovers and ignores
* the partial input.
*/
tokenEndpoint?: string;
/**
* From `beginConnect.issuer` — used to verify the JWT's `iss` claim.
* Pass alongside `tokenEndpoint`.
*/
issuer?: string;
/** Test seam. */
fetchImpl?: typeof fetch;
}
export interface CompleteConnectResult {
/** OAuth 2.1 access token (signed JWT). Treat as a credential. */
accessToken: string;
/** OAuth 2.1 refresh token, when offline_access was granted. */
refreshToken: string | null;
/**
* ms epoch. Derived from `expires_in` at exchange time; falls back
* to one hour from now when the token endpoint omits `expires_in`,
* so callers scheduling refresh against a server with a non-standard
* lifetime should re-check expiry from the JWT itself.
*/
expiresAt: number;
/** `https://atlas.useatlas.dev/workspace_id` claim from the JWT. */
workspaceId: string;
/**
* `https://atlas.useatlas.dev/workspace_ids` plural claim (#2196) — the
* complete set of workspaces this token grants access to, in the order
* the server emitted them. Empty array when the token was minted for
* a user belonging to exactly one workspace (the server omits the
* plural claim in that case). Always a stable type so embedders
* rendering a workspace picker don't need to null-check.
*
* Typed `ReadonlyArray<string>` so consumers can't mutate the
* SDK-owned array in place (the value is held in `useState` inside
* `useMcpConnect`; an in-place `.sort()` would silently mutate React
* state). Pipes cleanly into `buildConfig({ workspaces })` whose
* input shape is the same.
*
* The runtime authorization layer at the MCP edge does NOT rely on
* this list — membership is re-checked against the live grants table
* on every request so revocation is immediate. Treat the array as a
* UX affordance, not a security boundary.
*/
workspaces: ReadonlyArray<string>;
}
export type McpClientId = "claude-desktop" | "cursor" | "continue" | "chatgpt" | "generic";
export interface BuildConfigOptions {
client: McpClientId;
apiUrl: string;
accessToken: string;
/**
* Workspace pinned in the connection URL. Even for multi-workspace
* tokens this is required — the hosted MCP edge mounts at
* `/mcp/{workspace_id}/sse`. For multi-workspace setups pass the
* default-workspace id (typically the singular claim from
* `completeConnect`); per-request overrides happen via the
* `X-Atlas-Workspace` header.
*/
workspaceId: string;
/** Override the `mcpServers["..."]` key. Defaults to `"atlas"`. */
serverName?: string;
/**
* Multi-workspace opt-in (#2196). Pass the full list of workspaces
* this token grants access to (the `workspaces` field from
* `completeConnect`'s result). When non-empty, the emitted block
* gains an `env: { ATLAS_DEFAULT_WORKSPACE: <workspaceId> }` slot so
* future MCP-client framework wrappers can bridge it into the
* `X-Atlas-Default-Workspace` header (priority 2 in the edge's
* resolution chain). Output matches the CLI's hosted-config writer
* so SDK and CLI emit identical config blocks for the same token.
*
* **Wire-shape note.** [#2073's recommendation A](https://github.com/AtlasDevHQ/atlas/issues/2073)
* sketched `url: "https://mcp.useatlas.dev/sse"` without a workspace
* in the path, but the implemented hosted MCP endpoint mounts at
* `/mcp/{workspace_id}/sse` and resolves per-request overrides via
* the `X-Atlas-Workspace` header. The SDK emits the implemented
* shape — a single config block, one default workspace in the
* path, and the env hint for per-request overrides.
*
* Omit (or pass an empty array) for the legacy single-workspace
* shape — backward-compatible with every caller pre-#2196.
*/
workspaces?: ReadonlyArray<string>;
}
export interface McpHttpServer {
url: string;
headers: {
Authorization: string;
};
/**
* Multi-workspace hint block (#2196). Present only when `buildConfig`
* was called with a non-empty `workspaces` array — the legacy
* single-workspace shape omits the field entirely so the
* JSON-serialized server block is byte-identical to pre-#2196
* output. (The outer `McpClientConfig` carries a `kind` discriminator
* intended to be dropped before JSON output; see `stripKind` in the
* worked example for the standard call-site pattern.)
*/
env?: {
ATLAS_DEFAULT_WORKSPACE: string;
};
}
/**
* Discriminated by `client`:
* - `"generic"` returns the bare server block — `{ url, headers }`.
* - everything else returns the wrapped block —
* `{ mcpServers: { [serverName]: McpHttpServer } }`.
*
* Splitting these as a tagged union (instead of one bag with optional
* fields) means a consumer that destructures `cfg` gets only the
* fields that are actually present for the chosen client, without
* needing runtime guards.
*/
export interface McpWrappedConfig {
readonly kind: "wrapped";
readonly mcpServers: Record<string, McpHttpServer>;
}
export interface McpBareConfig extends McpHttpServer {
readonly kind: "bare";
}
export type McpClientConfig = McpWrappedConfig | McpBareConfig;
export interface ConnectMachineToMachineOptions {
apiUrl: string;
clientId: string;
clientSecret: string;
scopes?: ReadonlyArray<string>;
fetchImpl?: typeof fetch;
}
export interface ConnectMachineToMachineResult {
accessToken: string;
expiresAt: number;
}
export declare function beginConnect(options: BeginConnectOptions): Promise<BeginConnectResult>;
export declare function completeConnect(options: CompleteConnectOptions): Promise<CompleteConnectResult>;
export declare function buildConfig(options: BuildConfigOptions): McpClientConfig;
/**
* Server-to-server flow using the OAuth `client_credentials` grant.
*
* Throws `AtlasMcpError(code: "grant_not_supported")` until the Atlas
* OAuth provider exposes the grant. The public surface is fixed; when
* the provider lands the grant (tracking issue: see roadmap), swap
* this body for the live exchange without changing the type.
*/
export declare function connectMachineToMachine(_options: ConnectMachineToMachineOptions): Promise<ConnectMachineToMachineResult>;
export interface ListAgentsResponse {
clients: Array<{
clientId: string;
clientName: string | null;
redirectUris: string[];
createdAt: string;
updatedAt: string | null;
disabled: boolean;
type: string | null;
lastUsedAt: string | null;
tokenCount: number;
tokenState: "active" | "reconnect_required" | "revoked";
}>;
deployMode: "self-hosted" | "saas";
}
export interface RevokeAgentResponse {
success: boolean;
tokensRevoked: number;
}
// ../oauth-helper/src/errors.ts
class OAuthHelperError extends Error {
code;
constructor(message, code, options) {
super(message, options);
this.name = "OAuthHelperError";
this.code = code;
}
}
// ../oauth-helper/src/_internal/http.ts
var FETCH_TIMEOUT_MS = 30 * 1000;
async function describeOAuthErrorBody(res) {
let raw;
try {
raw = await res.text();
} catch (err) {
return `<failed to read response body: ${err instanceof Error ? err.message : String(err)}>`;
}
if (!raw)
return "";
try {
const parsed = JSON.parse(raw);
const parts = [];
if (typeof parsed.error === "string" && parsed.error.length > 0) {
parts.push(parsed.error);
}
if (typeof parsed.error_description === "string" && parsed.error_description.length > 0) {
parts.push(parsed.error_description);
}
if (typeof parsed.error_uri === "string" && parsed.error_uri.length > 0) {
parts.push(`see ${parsed.error_uri}`);
}
if (parts.length > 0)
return parts.join(": ");
} catch {}
return raw.length > 1024 ? `${raw.slice(0, 1024)}…` : raw;
}
// ../oauth-helper/src/discover.ts
function stripTrailingSlashes(s) {
let i = s.length;
while (i > 0 && s[i - 1] === "/")
i--;
return i === s.length ? s : s.slice(0, i);
}
var DISCOVERY_PATH = "/.well-known/oauth-authorization-server/api/auth";
async function discover(apiUrl, options) {
const fetchImpl = options?.fetchImpl ?? fetch;
const url = `${stripTrailingSlashes(apiUrl)}${DISCOVERY_PATH}`;
let res;
try {
res = await fetchImpl(url, {
method: "GET",
headers: { Accept: "application/json" },
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
});
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
throw new OAuthHelperError(`Could not reach Atlas auth discovery at ${url}: ${msg}`, "discovery_failed", { cause: err });
}
if (!res.ok) {
throw new OAuthHelperError(`Atlas auth discovery returned ${res.status} for ${url}`, "discovery_failed");
}
const body = await res.json().catch((err) => {
throw new OAuthHelperError(`Atlas auth discovery body was not JSON: ${err instanceof Error ? err.message : String(err)}`, "discovery_failed", { cause: err });
});
if (typeof body.authorization_endpoint !== "string" || typeof body.token_endpoint !== "string" || typeof body.registration_endpoint !== "string" || typeof body.issuer !== "string") {
throw new OAuthHelperError(`Atlas auth discovery is missing one of: authorization_endpoint, token_endpoint, registration_endpoint, issuer`, "discovery_failed");
}
return {
authorization_endpoint: body.authorization_endpoint,
token_endpoint: body.token_endpoint,
registration_endpoint: body.registration_endpoint,
issuer: body.issuer
};
}
// ../oauth-helper/src/register.ts
async function register(metadata, params, options) {
const fetchImpl = options?.fetchImpl ?? fetch;
const body = {
client_name: params.clientName,
redirect_uris: [params.redirectUri],
grant_types: ["authorization_code", "refresh_token"],
response_types: ["code"],
scope: params.scopes.join(" "),
token_endpoint_auth_method: "none"
};
let res;
try {
res = await fetchImpl(metadata.registration_endpoint, {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json"
},
body: JSON.stringify(body),
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
});
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
throw new OAuthHelperError(`Dynamic Client Registration failed: ${msg}`, "registration_failed", { cause: err });
}
if (!res.ok) {
const detail = await describeOAuthErrorBody(res);
throw new OAuthHelperError(`Dynamic Client Registration returned ${res.status}${detail ? `: ${detail}` : ""}`, "registration_failed");
}
const data = await res.json().catch((err) => {
throw new OAuthHelperError(`Dynamic Client Registration response was not JSON: ${err instanceof Error ? err.message : String(err)}`, "registration_failed", { cause: err });
});
if (typeof data.client_id !== "string" || data.client_id.length === 0) {
throw new OAuthHelperError(`Dynamic Client Registration response missing client_id`, "registration_failed");
}
return data.client_id;
}
// ../oauth-helper/src/_internal/encoding.ts
function defaultRandomBytes(length) {
const buf = new Uint8Array(length);
crypto.getRandomValues(buf);
return buf;
}
function encodeBase64Url(bytes) {
let bin = "";
for (let i = 0;i < bytes.length; i++)
bin += String.fromCharCode(bytes[i]);
return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
}
// ../oauth-helper/src/pkce.ts
var VERIFIER_BYTES = 32;
var STATE_BYTES = 32;
async function generatePkce(options) {
const randomBytes = options?.randomBytesImpl ?? defaultRandomBytes;
const codeVerifier = encodeBase64Url(randomBytes(VERIFIER_BYTES));
const codeChallenge = await pkceChallenge(codeVerifier);
return { codeVerifier, codeChallenge, method: "S256" };
}
function generateState(options) {
const randomBytes = options?.randomBytesImpl ?? defaultRandomBytes;
return encodeBase64Url(randomBytes(STATE_BYTES));
}
async function pkceChallenge(verifier) {
const data = new TextEncoder().encode(verifier);
const digest = await crypto.subtle.digest("SHA-256", data);
return encodeBase64Url(new Uint8Array(digest));
}
// ../oauth-helper/src/authorize-url.ts
function buildAuthorizationUrl(params) {
const search = new URLSearchParams({
response_type: "code",
client_id: params.clientId,
redirect_uri: params.redirectUri,
scope: params.scopes.join(" "),
state: params.state,
code_challenge: params.codeChallenge,
code_challenge_method: "S256"
});
const sep = params.authorizationEndpoint.includes("?") ? "&" : "?";
return `${params.authorizationEndpoint}${sep}${search.toString()}`;
}
// ../oauth-helper/src/validate.ts
function validateHttpsUrl(input, code, label) {
let parsed;
try {
parsed = new URL(input);
} catch (err) {
throw new OAuthHelperError(`${label} is not a valid URL: ${input}`, code, { cause: err });
}
if (parsed.protocol === "https:")
return;
if (parsed.protocol === "http:" && (parsed.hostname === "127.0.0.1" || parsed.hostname === "localhost")) {
return;
}
throw new OAuthHelperError(`${label} must use https:// (or http://localhost for dev). Got: ${input}`, code);
}
function validateIssuerUrl(apiUrl) {
validateHttpsUrl(apiUrl, "invalid_api_url", "apiUrl");
}
function validateTokenEndpoint(tokenEndpoint) {
validateHttpsUrl(tokenEndpoint, "invalid_token_endpoint", "tokenEndpoint");
}
// ../oauth-helper/src/exchange.ts
async function exchangeCode(params, options) {
validateTokenEndpoint(params.tokenEndpoint);
const fetchImpl = options?.fetchImpl ?? fetch;
const body = new URLSearchParams({
grant_type: "authorization_code",
code: params.code,
redirect_uri: params.redirectUri,
client_id: params.clientId,
code_verifier: params.codeVerifier
});
let res;
try {
res = await fetchImpl(params.tokenEndpoint, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
Accept: "application/json"
},
body: body.toString(),
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
});
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
throw new OAuthHelperError(`Token exchange failed: ${msg}`, "token_exchange_failed", { cause: err });
}
if (!res.ok) {
const detail = await describeOAuthErrorBody(res);
throw new OAuthHelperError(`Token endpoint returned ${res.status}${detail ? `: ${detail}` : ""}`, "token_exchange_failed");
}
const data = await res.json().catch((err) => {
throw new OAuthHelperError(`Token endpoint response was not JSON: ${err instanceof Error ? err.message : String(err)}`, "token_exchange_failed", { cause: err });
});
if (typeof data.access_token !== "string" || data.access_token.length === 0) {
throw new OAuthHelperError(`Token endpoint response missing access_token`, "token_exchange_failed");
}
return {
access_token: data.access_token,
refresh_token: typeof data.refresh_token === "string" ? data.refresh_token : undefined,
token_type: typeof data.token_type === "string" ? data.token_type : undefined,
expires_in: typeof data.expires_in === "number" ? data.expires_in : undefined,
scope: typeof data.scope === "string" ? data.scope : undefined
};
}
// ../oauth-helper/src/jwt.ts
function decodeJwtPayload(jwtToken) {
const parts = jwtToken.split(".");
if (parts.length !== 3) {
throw new OAuthHelperError(`Access token is not a JWT (expected 3 parts, got ${parts.length})`, "malformed_jwt");
}
try {
const json = atob(parts[1].replace(/-/g, "+").replace(/_/g, "/"));
return JSON.parse(json);
} catch (err) {
throw new OAuthHelperError(`Could not decode JWT payload: ${err instanceof Error ? err.message : String(err)}`, "malformed_jwt", { cause: err });
}
}
function enforceIssuer(payload, expectedIssuer) {
const iss = payload.iss;
if (typeof iss !== "string" || iss.length === 0) {
throw new OAuthHelperError(`Access token has no \`iss\` claim — refusing to trust an unsigned-issuer token.`, "issuer_mismatch");
}
if (iss !== expectedIssuer) {
throw new OAuthHelperError(`Access token issuer mismatch: discovered \`${expectedIssuer}\`, token claims \`${iss}\`.`, "issuer_mismatch");
}
}
// src/mcp.ts
class AtlasMcpError extends Error {
code;
constructor(message, code, options) {
super(message, options);
this.name = "AtlasMcpError";
this.code = code;
}
}
async function liftHelper(fn) {
try {
return await fn();
} catch (err) {
if (err instanceof OAuthHelperError) {
throw new AtlasMcpError(err.message, err.code, { cause: err });
}
throw err;
}
}
function liftHelperSync(fn) {
try {
return fn();
} catch (err) {
if (err instanceof OAuthHelperError) {
throw new AtlasMcpError(err.message, err.code, { cause: err });
}
throw err;
}
}
var DEFAULT_SCOPES = ["mcp:read", "offline_access"];
var WORKSPACE_CLAIM = "https://atlas.useatlas.dev/workspace_id";
var WORKSPACES_CLAIM = "https://atlas.useatlas.dev/workspace_ids";
async function beginConnect(options) {
const apiUrl = stripTrailingSlashes2(options.apiUrl);
liftHelperSync(() => validateIssuerUrl(apiUrl));
const fetchImpl = options.fetchImpl ?? fetch;
const randomBytesImpl = options.randomBytesImpl;
const scopes = options.scopes ?? DEFAULT_SCOPES;
return liftHelper(async () => {
const metadata = await discover(apiUrl, { fetchImpl });
const state = generateState({ randomBytesImpl });
const { codeVerifier, codeChallenge } = await generatePkce({ randomBytesImpl });
const clientId = await register(metadata, {
redirectUri: options.redirectUri,
clientName: options.clientName,
scopes
}, { fetchImpl });
const authorizationUrl = buildAuthorizationUrl({
authorizationEndpoint: metadata.authorization_endpoint,
clientId,
redirectUri: options.redirectUri,
state,
codeChallenge,
scopes
});
return {
authorizationUrl,
state,
codeVerifier,
clientId,
tokenEndpoint: metadata.token_endpoint,
issuer: metadata.issuer
};
});
}
async function completeConnect(options) {
if (options.state !== options.expectedState) {
throw new AtlasMcpError(`OAuth state mismatch — possible CSRF. Got \`${options.state}\`, expected the value returned from beginConnect.`, "callback_state_mismatch");
}
if (!options.code) {
throw new AtlasMcpError(`Authorization callback was missing the \`code\` parameter.`, "callback_missing_code");
}
const apiUrl = stripTrailingSlashes2(options.apiUrl);
liftHelperSync(() => validateIssuerUrl(apiUrl));
const fetchImpl = options.fetchImpl ?? fetch;
return liftHelper(async () => {
let tokenEndpoint = options.tokenEndpoint;
let issuer = options.issuer;
if (!tokenEndpoint || !issuer) {
const metadata = await discover(apiUrl, { fetchImpl });
tokenEndpoint = tokenEndpoint ?? metadata.token_endpoint;
issuer = issuer ?? metadata.issuer;
}
validateTokenEndpoint(tokenEndpoint);
const tokenResponse = await exchangeCode({
tokenEndpoint,
clientId: options.clientId,
redirectUri: options.redirectUri,
code: options.code,
codeVerifier: options.codeVerifier
}, { fetchImpl });
const claims = decodeJwtPayload(tokenResponse.access_token);
enforceIssuer(claims, issuer);
const workspaceId = extractWorkspaceClaim(claims);
const workspaces = extractWorkspacesClaim(claims);
const expiresIn = typeof tokenResponse.expires_in === "number" && tokenResponse.expires_in > 0 ? tokenResponse.expires_in : 3600;
return {
accessToken: tokenResponse.access_token,
refreshToken: tokenResponse.refresh_token ?? null,
expiresAt: Date.now() + expiresIn * 1000,
workspaceId,
workspaces
};
});
}
var SERVER_NAME_DEFAULT = "atlas";
function buildConfig(options) {
const apiUrl = stripTrailingSlashes2(options.apiUrl);
const url = `${apiUrl}/mcp/${encodeURIComponent(options.workspaceId)}/sse`;
const isMultiWorkspace = (options.workspaces?.length ?? 0) > 0;
if (isMultiWorkspace && !options.workspaces.includes(options.workspaceId)) {
throw new AtlasMcpError(`buildConfig: workspaceId \`${options.workspaceId}\` is not in the granted workspaces list — pass a default that is one of: ${options.workspaces.join(", ")}.`, "workspace_not_in_grant_list");
}
const block = isMultiWorkspace ? {
url,
headers: { Authorization: `Bearer ${options.accessToken}` },
env: { ATLAS_DEFAULT_WORKSPACE: options.workspaceId }
} : {
url,
headers: { Authorization: `Bearer ${options.accessToken}` }
};
const name = options.serverName ?? SERVER_NAME_DEFAULT;
switch (options.client) {
case "generic":
return isMultiWorkspace ? {
kind: "bare",
url: block.url,
headers: block.headers,
env: block.env
} : { kind: "bare", url: block.url, headers: block.headers };
case "claude-desktop":
case "cursor":
case "continue":
case "chatgpt":
return { kind: "wrapped", mcpServers: { [name]: block } };
default:
return assertNever(options.client);
}
}
function assertNever(x) {
throw new Error(`Unhandled MCP client id: ${String(x)}`);
}
async function connectMachineToMachine(_options) {
throw new AtlasMcpError("client_credentials grant is not yet enabled on the Atlas OAuth provider. Use the authorization-code flow (beginConnect / completeConnect) for now.", "grant_not_supported");
}
function stripTrailingSlashes2(s) {
let i = s.length;
while (i > 0 && s[i - 1] === "/")
i--;
return i === s.length ? s : s.slice(0, i);
}
function extractWorkspaceClaim(payload) {
const claim = payload[WORKSPACE_CLAIM];
if (typeof claim !== "string" || claim.length === 0) {
throw new AtlasMcpError(`Access token is missing the ${WORKSPACE_CLAIM} claim — was the token issued for an MCP scope?`, "missing_workspace_claim");
}
return claim;
}
function extractWorkspacesClaim(payload) {
const raw = payload[WORKSPACES_CLAIM];
if (raw === undefined)
return [];
if (!Array.isArray(raw)) {
console.warn(`[@useatlas/sdk] ${WORKSPACES_CLAIM} claim was present but not an array (got ${typeof raw}); falling back to single-workspace.`);
return [];
}
const filtered = raw.filter((entry) => typeof entry === "string" && entry.length > 0);
if (filtered.length !== raw.length) {
const dropped = raw.length - filtered.length;
console.warn(`[@useatlas/sdk] ${WORKSPACES_CLAIM} claim contained ${dropped} non-string or empty entr${dropped === 1 ? "y" : "ies"}; using only the ${filtered.length} valid entr${filtered.length === 1 ? "y" : "ies"}.`);
}
return filtered;
}
export {
connectMachineToMachine,
completeConnect,
buildConfig,
beginConnect,
AtlasMcpError
};
+27
-0

@@ -9,2 +9,3 @@ /**

import type { AuthMode, ChatErrorCode, Conversation, ConversationWithMessages, ConnectionHealth, ConnectionInfo, DeliveryChannel, ActionApprovalMode, ActionStatus, RollbackInfo, Recipient, ScheduledTask, ScheduledTaskWithRuns, ScheduledTaskRun, StarterPromptsResponse, TableInfo } from "@useatlas/types";
import { type BeginConnectOptions, type BeginConnectResult, type BuildConfigOptions, type CompleteConnectOptions, type CompleteConnectResult, type ConnectMachineToMachineOptions, type ConnectMachineToMachineResult, type ListAgentsResponse, type McpClientConfig, type RevokeAgentResponse } from "./mcp";
/** @deprecated Use `Recipient` instead. */

@@ -468,4 +469,30 @@ export type ScheduledTaskRecipient = Recipient;

chat(messages: ChatMessage[], opts?: ChatOptions): Promise<Response>;
/**
* MCP onboarding helpers pre-bound to the client's `baseUrl` so
* embedders don't repeat the apiUrl. The unbound versions live at
* `@useatlas/sdk/mcp` for callers who want to share an `apiUrl`
* across multiple clients or skip the AtlasClient construction.
*/
mcp: {
/** Initiate the OAuth 2.1 + DCR flow. Returns the URL to open + state to persist. */
beginConnect(opts: Omit<BeginConnectOptions, "apiUrl">): Promise<BeginConnectResult>;
/** Exchange the OAuth `code` for an access token + workspace id. */
completeConnect(opts: Omit<CompleteConnectOptions, "apiUrl">): Promise<CompleteConnectResult>;
/** Build a paste-ready config block for the requested MCP client. */
buildConfig(opts: Omit<BuildConfigOptions, "apiUrl">): McpClientConfig;
/** Server-to-server flow (currently throws — gated on #2024). */
connectMachineToMachine(opts: Omit<ConnectMachineToMachineOptions, "apiUrl">): Promise<ConnectMachineToMachineResult>;
/**
* List the calling user's connected MCP agents (OAuth clients) for
* the active workspace. Wraps `GET /api/v1/me/oauth-clients`.
*/
listAgents(): Promise<ListAgentsResponse>;
/**
* Revoke one of the calling user's MCP agents. Wraps
* `POST /api/v1/me/oauth-clients/:id/revoke`.
*/
revokeAgent(clientId: string): Promise<RevokeAgentResponse>;
};
};
/** The return type of `createAtlasClient`. */
export type AtlasClient = ReturnType<typeof createAtlasClient>;

@@ -0,1 +1,422 @@

// ../oauth-helper/src/errors.ts
class OAuthHelperError extends Error {
code;
constructor(message, code, options) {
super(message, options);
this.name = "OAuthHelperError";
this.code = code;
}
}
// ../oauth-helper/src/_internal/http.ts
var FETCH_TIMEOUT_MS = 30 * 1000;
async function describeOAuthErrorBody(res) {
let raw;
try {
raw = await res.text();
} catch (err) {
return `<failed to read response body: ${err instanceof Error ? err.message : String(err)}>`;
}
if (!raw)
return "";
try {
const parsed = JSON.parse(raw);
const parts = [];
if (typeof parsed.error === "string" && parsed.error.length > 0) {
parts.push(parsed.error);
}
if (typeof parsed.error_description === "string" && parsed.error_description.length > 0) {
parts.push(parsed.error_description);
}
if (typeof parsed.error_uri === "string" && parsed.error_uri.length > 0) {
parts.push(`see ${parsed.error_uri}`);
}
if (parts.length > 0)
return parts.join(": ");
} catch {}
return raw.length > 1024 ? `${raw.slice(0, 1024)}…` : raw;
}
// ../oauth-helper/src/discover.ts
function stripTrailingSlashes(s) {
let i = s.length;
while (i > 0 && s[i - 1] === "/")
i--;
return i === s.length ? s : s.slice(0, i);
}
var DISCOVERY_PATH = "/.well-known/oauth-authorization-server/api/auth";
async function discover(apiUrl, options) {
const fetchImpl = options?.fetchImpl ?? fetch;
const url = `${stripTrailingSlashes(apiUrl)}${DISCOVERY_PATH}`;
let res;
try {
res = await fetchImpl(url, {
method: "GET",
headers: { Accept: "application/json" },
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
});
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
throw new OAuthHelperError(`Could not reach Atlas auth discovery at ${url}: ${msg}`, "discovery_failed", { cause: err });
}
if (!res.ok) {
throw new OAuthHelperError(`Atlas auth discovery returned ${res.status} for ${url}`, "discovery_failed");
}
const body = await res.json().catch((err) => {
throw new OAuthHelperError(`Atlas auth discovery body was not JSON: ${err instanceof Error ? err.message : String(err)}`, "discovery_failed", { cause: err });
});
if (typeof body.authorization_endpoint !== "string" || typeof body.token_endpoint !== "string" || typeof body.registration_endpoint !== "string" || typeof body.issuer !== "string") {
throw new OAuthHelperError(`Atlas auth discovery is missing one of: authorization_endpoint, token_endpoint, registration_endpoint, issuer`, "discovery_failed");
}
return {
authorization_endpoint: body.authorization_endpoint,
token_endpoint: body.token_endpoint,
registration_endpoint: body.registration_endpoint,
issuer: body.issuer
};
}
// ../oauth-helper/src/register.ts
async function register(metadata, params, options) {
const fetchImpl = options?.fetchImpl ?? fetch;
const body = {
client_name: params.clientName,
redirect_uris: [params.redirectUri],
grant_types: ["authorization_code", "refresh_token"],
response_types: ["code"],
scope: params.scopes.join(" "),
token_endpoint_auth_method: "none"
};
let res;
try {
res = await fetchImpl(metadata.registration_endpoint, {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json"
},
body: JSON.stringify(body),
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
});
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
throw new OAuthHelperError(`Dynamic Client Registration failed: ${msg}`, "registration_failed", { cause: err });
}
if (!res.ok) {
const detail = await describeOAuthErrorBody(res);
throw new OAuthHelperError(`Dynamic Client Registration returned ${res.status}${detail ? `: ${detail}` : ""}`, "registration_failed");
}
const data = await res.json().catch((err) => {
throw new OAuthHelperError(`Dynamic Client Registration response was not JSON: ${err instanceof Error ? err.message : String(err)}`, "registration_failed", { cause: err });
});
if (typeof data.client_id !== "string" || data.client_id.length === 0) {
throw new OAuthHelperError(`Dynamic Client Registration response missing client_id`, "registration_failed");
}
return data.client_id;
}
// ../oauth-helper/src/_internal/encoding.ts
function defaultRandomBytes(length) {
const buf = new Uint8Array(length);
crypto.getRandomValues(buf);
return buf;
}
function encodeBase64Url(bytes) {
let bin = "";
for (let i = 0;i < bytes.length; i++)
bin += String.fromCharCode(bytes[i]);
return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
}
// ../oauth-helper/src/pkce.ts
var VERIFIER_BYTES = 32;
var STATE_BYTES = 32;
async function generatePkce(options) {
const randomBytes = options?.randomBytesImpl ?? defaultRandomBytes;
const codeVerifier = encodeBase64Url(randomBytes(VERIFIER_BYTES));
const codeChallenge = await pkceChallenge(codeVerifier);
return { codeVerifier, codeChallenge, method: "S256" };
}
function generateState(options) {
const randomBytes = options?.randomBytesImpl ?? defaultRandomBytes;
return encodeBase64Url(randomBytes(STATE_BYTES));
}
async function pkceChallenge(verifier) {
const data = new TextEncoder().encode(verifier);
const digest = await crypto.subtle.digest("SHA-256", data);
return encodeBase64Url(new Uint8Array(digest));
}
// ../oauth-helper/src/authorize-url.ts
function buildAuthorizationUrl(params) {
const search = new URLSearchParams({
response_type: "code",
client_id: params.clientId,
redirect_uri: params.redirectUri,
scope: params.scopes.join(" "),
state: params.state,
code_challenge: params.codeChallenge,
code_challenge_method: "S256"
});
const sep = params.authorizationEndpoint.includes("?") ? "&" : "?";
return `${params.authorizationEndpoint}${sep}${search.toString()}`;
}
// ../oauth-helper/src/validate.ts
function validateHttpsUrl(input, code, label) {
let parsed;
try {
parsed = new URL(input);
} catch (err) {
throw new OAuthHelperError(`${label} is not a valid URL: ${input}`, code, { cause: err });
}
if (parsed.protocol === "https:")
return;
if (parsed.protocol === "http:" && (parsed.hostname === "127.0.0.1" || parsed.hostname === "localhost")) {
return;
}
throw new OAuthHelperError(`${label} must use https:// (or http://localhost for dev). Got: ${input}`, code);
}
function validateIssuerUrl(apiUrl) {
validateHttpsUrl(apiUrl, "invalid_api_url", "apiUrl");
}
function validateTokenEndpoint(tokenEndpoint) {
validateHttpsUrl(tokenEndpoint, "invalid_token_endpoint", "tokenEndpoint");
}
// ../oauth-helper/src/exchange.ts
async function exchangeCode(params, options) {
validateTokenEndpoint(params.tokenEndpoint);
const fetchImpl = options?.fetchImpl ?? fetch;
const body = new URLSearchParams({
grant_type: "authorization_code",
code: params.code,
redirect_uri: params.redirectUri,
client_id: params.clientId,
code_verifier: params.codeVerifier
});
let res;
try {
res = await fetchImpl(params.tokenEndpoint, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
Accept: "application/json"
},
body: body.toString(),
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
});
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
throw new OAuthHelperError(`Token exchange failed: ${msg}`, "token_exchange_failed", { cause: err });
}
if (!res.ok) {
const detail = await describeOAuthErrorBody(res);
throw new OAuthHelperError(`Token endpoint returned ${res.status}${detail ? `: ${detail}` : ""}`, "token_exchange_failed");
}
const data = await res.json().catch((err) => {
throw new OAuthHelperError(`Token endpoint response was not JSON: ${err instanceof Error ? err.message : String(err)}`, "token_exchange_failed", { cause: err });
});
if (typeof data.access_token !== "string" || data.access_token.length === 0) {
throw new OAuthHelperError(`Token endpoint response missing access_token`, "token_exchange_failed");
}
return {
access_token: data.access_token,
refresh_token: typeof data.refresh_token === "string" ? data.refresh_token : undefined,
token_type: typeof data.token_type === "string" ? data.token_type : undefined,
expires_in: typeof data.expires_in === "number" ? data.expires_in : undefined,
scope: typeof data.scope === "string" ? data.scope : undefined
};
}
// ../oauth-helper/src/jwt.ts
function decodeJwtPayload(jwtToken) {
const parts = jwtToken.split(".");
if (parts.length !== 3) {
throw new OAuthHelperError(`Access token is not a JWT (expected 3 parts, got ${parts.length})`, "malformed_jwt");
}
try {
const json = atob(parts[1].replace(/-/g, "+").replace(/_/g, "/"));
return JSON.parse(json);
} catch (err) {
throw new OAuthHelperError(`Could not decode JWT payload: ${err instanceof Error ? err.message : String(err)}`, "malformed_jwt", { cause: err });
}
}
function enforceIssuer(payload, expectedIssuer) {
const iss = payload.iss;
if (typeof iss !== "string" || iss.length === 0) {
throw new OAuthHelperError(`Access token has no \`iss\` claim — refusing to trust an unsigned-issuer token.`, "issuer_mismatch");
}
if (iss !== expectedIssuer) {
throw new OAuthHelperError(`Access token issuer mismatch: discovered \`${expectedIssuer}\`, token claims \`${iss}\`.`, "issuer_mismatch");
}
}
// src/mcp.ts
class AtlasMcpError extends Error {
code;
constructor(message, code, options) {
super(message, options);
this.name = "AtlasMcpError";
this.code = code;
}
}
async function liftHelper(fn) {
try {
return await fn();
} catch (err) {
if (err instanceof OAuthHelperError) {
throw new AtlasMcpError(err.message, err.code, { cause: err });
}
throw err;
}
}
function liftHelperSync(fn) {
try {
return fn();
} catch (err) {
if (err instanceof OAuthHelperError) {
throw new AtlasMcpError(err.message, err.code, { cause: err });
}
throw err;
}
}
var DEFAULT_SCOPES = ["mcp:read", "offline_access"];
var WORKSPACE_CLAIM = "https://atlas.useatlas.dev/workspace_id";
var WORKSPACES_CLAIM = "https://atlas.useatlas.dev/workspace_ids";
async function beginConnect(options) {
const apiUrl = stripTrailingSlashes2(options.apiUrl);
liftHelperSync(() => validateIssuerUrl(apiUrl));
const fetchImpl = options.fetchImpl ?? fetch;
const randomBytesImpl = options.randomBytesImpl;
const scopes = options.scopes ?? DEFAULT_SCOPES;
return liftHelper(async () => {
const metadata = await discover(apiUrl, { fetchImpl });
const state = generateState({ randomBytesImpl });
const { codeVerifier, codeChallenge } = await generatePkce({ randomBytesImpl });
const clientId = await register(metadata, {
redirectUri: options.redirectUri,
clientName: options.clientName,
scopes
}, { fetchImpl });
const authorizationUrl = buildAuthorizationUrl({
authorizationEndpoint: metadata.authorization_endpoint,
clientId,
redirectUri: options.redirectUri,
state,
codeChallenge,
scopes
});
return {
authorizationUrl,
state,
codeVerifier,
clientId,
tokenEndpoint: metadata.token_endpoint,
issuer: metadata.issuer
};
});
}
async function completeConnect(options) {
if (options.state !== options.expectedState) {
throw new AtlasMcpError(`OAuth state mismatch — possible CSRF. Got \`${options.state}\`, expected the value returned from beginConnect.`, "callback_state_mismatch");
}
if (!options.code) {
throw new AtlasMcpError(`Authorization callback was missing the \`code\` parameter.`, "callback_missing_code");
}
const apiUrl = stripTrailingSlashes2(options.apiUrl);
liftHelperSync(() => validateIssuerUrl(apiUrl));
const fetchImpl = options.fetchImpl ?? fetch;
return liftHelper(async () => {
let tokenEndpoint = options.tokenEndpoint;
let issuer = options.issuer;
if (!tokenEndpoint || !issuer) {
const metadata = await discover(apiUrl, { fetchImpl });
tokenEndpoint = tokenEndpoint ?? metadata.token_endpoint;
issuer = issuer ?? metadata.issuer;
}
validateTokenEndpoint(tokenEndpoint);
const tokenResponse = await exchangeCode({
tokenEndpoint,
clientId: options.clientId,
redirectUri: options.redirectUri,
code: options.code,
codeVerifier: options.codeVerifier
}, { fetchImpl });
const claims = decodeJwtPayload(tokenResponse.access_token);
enforceIssuer(claims, issuer);
const workspaceId = extractWorkspaceClaim(claims);
const workspaces = extractWorkspacesClaim(claims);
const expiresIn = typeof tokenResponse.expires_in === "number" && tokenResponse.expires_in > 0 ? tokenResponse.expires_in : 3600;
return {
accessToken: tokenResponse.access_token,
refreshToken: tokenResponse.refresh_token ?? null,
expiresAt: Date.now() + expiresIn * 1000,
workspaceId,
workspaces
};
});
}
var SERVER_NAME_DEFAULT = "atlas";
function buildConfig(options) {
const apiUrl = stripTrailingSlashes2(options.apiUrl);
const url = `${apiUrl}/mcp/${encodeURIComponent(options.workspaceId)}/sse`;
const isMultiWorkspace = (options.workspaces?.length ?? 0) > 0;
if (isMultiWorkspace && !options.workspaces.includes(options.workspaceId)) {
throw new AtlasMcpError(`buildConfig: workspaceId \`${options.workspaceId}\` is not in the granted workspaces list — pass a default that is one of: ${options.workspaces.join(", ")}.`, "workspace_not_in_grant_list");
}
const block = isMultiWorkspace ? {
url,
headers: { Authorization: `Bearer ${options.accessToken}` },
env: { ATLAS_DEFAULT_WORKSPACE: options.workspaceId }
} : {
url,
headers: { Authorization: `Bearer ${options.accessToken}` }
};
const name = options.serverName ?? SERVER_NAME_DEFAULT;
switch (options.client) {
case "generic":
return isMultiWorkspace ? {
kind: "bare",
url: block.url,
headers: block.headers,
env: block.env
} : { kind: "bare", url: block.url, headers: block.headers };
case "claude-desktop":
case "cursor":
case "continue":
case "chatgpt":
return { kind: "wrapped", mcpServers: { [name]: block } };
default:
return assertNever(options.client);
}
}
function assertNever(x) {
throw new Error(`Unhandled MCP client id: ${String(x)}`);
}
async function connectMachineToMachine(_options) {
throw new AtlasMcpError("client_credentials grant is not yet enabled on the Atlas OAuth provider. Use the authorization-code flow (beginConnect / completeConnect) for now.", "grant_not_supported");
}
function stripTrailingSlashes2(s) {
let i = s.length;
while (i > 0 && s[i - 1] === "/")
i--;
return i === s.length ? s : s.slice(0, i);
}
function extractWorkspaceClaim(payload) {
const claim = payload[WORKSPACE_CLAIM];
if (typeof claim !== "string" || claim.length === 0) {
throw new AtlasMcpError(`Access token is missing the ${WORKSPACE_CLAIM} claim — was the token issued for an MCP scope?`, "missing_workspace_claim");
}
return claim;
}
function extractWorkspacesClaim(payload) {
const raw = payload[WORKSPACES_CLAIM];
if (raw === undefined)
return [];
if (!Array.isArray(raw)) {
console.warn(`[@useatlas/sdk] ${WORKSPACES_CLAIM} claim was present but not an array (got ${typeof raw}); falling back to single-workspace.`);
return [];
}
const filtered = raw.filter((entry) => typeof entry === "string" && entry.length > 0);
if (filtered.length !== raw.length) {
const dropped = raw.length - filtered.length;
console.warn(`[@useatlas/sdk] ${WORKSPACES_CLAIM} claim contained ${dropped} non-string or empty entr${dropped === 1 ? "y" : "ies"}; using only the ${filtered.length} valid entr${filtered.length === 1 ? "y" : "ies"}.`);
}
return filtered;
}
// src/client.ts

@@ -492,2 +913,24 @@ import { isChatErrorCode, isRetryableError } from "@useatlas/types";

return res;
},
mcp: {
async beginConnect(opts) {
return beginConnect({ apiUrl: base, ...opts });
},
async completeConnect(opts) {
return completeConnect({ apiUrl: base, ...opts });
},
buildConfig(opts) {
return buildConfig({ apiUrl: base, ...opts });
},
async connectMachineToMachine(opts) {
return connectMachineToMachine({ apiUrl: base, ...opts });
},
async listAgents() {
const res = await get("/api/v1/me/oauth-clients");
return unwrap(res);
},
async revokeAgent(clientId) {
const res = await post(`/api/v1/me/oauth-clients/${encodeURIComponent(clientId)}/revoke`, {});
return unwrap(res);
}
}

@@ -494,0 +937,0 @@ };

+1
-0

@@ -18,1 +18,2 @@ /**

export { fetchStarterPrompts, type FetchStarterPromptsConfig, type FetchStarterPromptsCredentials, } from "./fetch-starter-prompts";
export { AtlasMcpError, beginConnect, buildConfig, completeConnect, connectMachineToMachine, type AtlasMcpErrorCode, type BeginConnectOptions, type BeginConnectResult, type BuildConfigOptions, type CompleteConnectOptions, type CompleteConnectResult, type ConnectMachineToMachineOptions, type ConnectMachineToMachineResult, type ListAgentsResponse, type McpBareConfig, type McpClientConfig, type McpClientId, type McpHttpServer, type McpWrappedConfig, type RevokeAgentResponse, } from "./mcp";

@@ -0,1 +1,422 @@

// ../oauth-helper/src/errors.ts
class OAuthHelperError extends Error {
code;
constructor(message, code, options) {
super(message, options);
this.name = "OAuthHelperError";
this.code = code;
}
}
// ../oauth-helper/src/_internal/http.ts
var FETCH_TIMEOUT_MS = 30 * 1000;
async function describeOAuthErrorBody(res) {
let raw;
try {
raw = await res.text();
} catch (err) {
return `<failed to read response body: ${err instanceof Error ? err.message : String(err)}>`;
}
if (!raw)
return "";
try {
const parsed = JSON.parse(raw);
const parts = [];
if (typeof parsed.error === "string" && parsed.error.length > 0) {
parts.push(parsed.error);
}
if (typeof parsed.error_description === "string" && parsed.error_description.length > 0) {
parts.push(parsed.error_description);
}
if (typeof parsed.error_uri === "string" && parsed.error_uri.length > 0) {
parts.push(`see ${parsed.error_uri}`);
}
if (parts.length > 0)
return parts.join(": ");
} catch {}
return raw.length > 1024 ? `${raw.slice(0, 1024)}…` : raw;
}
// ../oauth-helper/src/discover.ts
function stripTrailingSlashes(s) {
let i = s.length;
while (i > 0 && s[i - 1] === "/")
i--;
return i === s.length ? s : s.slice(0, i);
}
var DISCOVERY_PATH = "/.well-known/oauth-authorization-server/api/auth";
async function discover(apiUrl, options) {
const fetchImpl = options?.fetchImpl ?? fetch;
const url = `${stripTrailingSlashes(apiUrl)}${DISCOVERY_PATH}`;
let res;
try {
res = await fetchImpl(url, {
method: "GET",
headers: { Accept: "application/json" },
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
});
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
throw new OAuthHelperError(`Could not reach Atlas auth discovery at ${url}: ${msg}`, "discovery_failed", { cause: err });
}
if (!res.ok) {
throw new OAuthHelperError(`Atlas auth discovery returned ${res.status} for ${url}`, "discovery_failed");
}
const body = await res.json().catch((err) => {
throw new OAuthHelperError(`Atlas auth discovery body was not JSON: ${err instanceof Error ? err.message : String(err)}`, "discovery_failed", { cause: err });
});
if (typeof body.authorization_endpoint !== "string" || typeof body.token_endpoint !== "string" || typeof body.registration_endpoint !== "string" || typeof body.issuer !== "string") {
throw new OAuthHelperError(`Atlas auth discovery is missing one of: authorization_endpoint, token_endpoint, registration_endpoint, issuer`, "discovery_failed");
}
return {
authorization_endpoint: body.authorization_endpoint,
token_endpoint: body.token_endpoint,
registration_endpoint: body.registration_endpoint,
issuer: body.issuer
};
}
// ../oauth-helper/src/register.ts
async function register(metadata, params, options) {
const fetchImpl = options?.fetchImpl ?? fetch;
const body = {
client_name: params.clientName,
redirect_uris: [params.redirectUri],
grant_types: ["authorization_code", "refresh_token"],
response_types: ["code"],
scope: params.scopes.join(" "),
token_endpoint_auth_method: "none"
};
let res;
try {
res = await fetchImpl(metadata.registration_endpoint, {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json"
},
body: JSON.stringify(body),
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
});
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
throw new OAuthHelperError(`Dynamic Client Registration failed: ${msg}`, "registration_failed", { cause: err });
}
if (!res.ok) {
const detail = await describeOAuthErrorBody(res);
throw new OAuthHelperError(`Dynamic Client Registration returned ${res.status}${detail ? `: ${detail}` : ""}`, "registration_failed");
}
const data = await res.json().catch((err) => {
throw new OAuthHelperError(`Dynamic Client Registration response was not JSON: ${err instanceof Error ? err.message : String(err)}`, "registration_failed", { cause: err });
});
if (typeof data.client_id !== "string" || data.client_id.length === 0) {
throw new OAuthHelperError(`Dynamic Client Registration response missing client_id`, "registration_failed");
}
return data.client_id;
}
// ../oauth-helper/src/_internal/encoding.ts
function defaultRandomBytes(length) {
const buf = new Uint8Array(length);
crypto.getRandomValues(buf);
return buf;
}
function encodeBase64Url(bytes) {
let bin = "";
for (let i = 0;i < bytes.length; i++)
bin += String.fromCharCode(bytes[i]);
return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
}
// ../oauth-helper/src/pkce.ts
var VERIFIER_BYTES = 32;
var STATE_BYTES = 32;
async function generatePkce(options) {
const randomBytes = options?.randomBytesImpl ?? defaultRandomBytes;
const codeVerifier = encodeBase64Url(randomBytes(VERIFIER_BYTES));
const codeChallenge = await pkceChallenge(codeVerifier);
return { codeVerifier, codeChallenge, method: "S256" };
}
function generateState(options) {
const randomBytes = options?.randomBytesImpl ?? defaultRandomBytes;
return encodeBase64Url(randomBytes(STATE_BYTES));
}
async function pkceChallenge(verifier) {
const data = new TextEncoder().encode(verifier);
const digest = await crypto.subtle.digest("SHA-256", data);
return encodeBase64Url(new Uint8Array(digest));
}
// ../oauth-helper/src/authorize-url.ts
function buildAuthorizationUrl(params) {
const search = new URLSearchParams({
response_type: "code",
client_id: params.clientId,
redirect_uri: params.redirectUri,
scope: params.scopes.join(" "),
state: params.state,
code_challenge: params.codeChallenge,
code_challenge_method: "S256"
});
const sep = params.authorizationEndpoint.includes("?") ? "&" : "?";
return `${params.authorizationEndpoint}${sep}${search.toString()}`;
}
// ../oauth-helper/src/validate.ts
function validateHttpsUrl(input, code, label) {
let parsed;
try {
parsed = new URL(input);
} catch (err) {
throw new OAuthHelperError(`${label} is not a valid URL: ${input}`, code, { cause: err });
}
if (parsed.protocol === "https:")
return;
if (parsed.protocol === "http:" && (parsed.hostname === "127.0.0.1" || parsed.hostname === "localhost")) {
return;
}
throw new OAuthHelperError(`${label} must use https:// (or http://localhost for dev). Got: ${input}`, code);
}
function validateIssuerUrl(apiUrl) {
validateHttpsUrl(apiUrl, "invalid_api_url", "apiUrl");
}
function validateTokenEndpoint(tokenEndpoint) {
validateHttpsUrl(tokenEndpoint, "invalid_token_endpoint", "tokenEndpoint");
}
// ../oauth-helper/src/exchange.ts
async function exchangeCode(params, options) {
validateTokenEndpoint(params.tokenEndpoint);
const fetchImpl = options?.fetchImpl ?? fetch;
const body = new URLSearchParams({
grant_type: "authorization_code",
code: params.code,
redirect_uri: params.redirectUri,
client_id: params.clientId,
code_verifier: params.codeVerifier
});
let res;
try {
res = await fetchImpl(params.tokenEndpoint, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
Accept: "application/json"
},
body: body.toString(),
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
});
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
throw new OAuthHelperError(`Token exchange failed: ${msg}`, "token_exchange_failed", { cause: err });
}
if (!res.ok) {
const detail = await describeOAuthErrorBody(res);
throw new OAuthHelperError(`Token endpoint returned ${res.status}${detail ? `: ${detail}` : ""}`, "token_exchange_failed");
}
const data = await res.json().catch((err) => {
throw new OAuthHelperError(`Token endpoint response was not JSON: ${err instanceof Error ? err.message : String(err)}`, "token_exchange_failed", { cause: err });
});
if (typeof data.access_token !== "string" || data.access_token.length === 0) {
throw new OAuthHelperError(`Token endpoint response missing access_token`, "token_exchange_failed");
}
return {
access_token: data.access_token,
refresh_token: typeof data.refresh_token === "string" ? data.refresh_token : undefined,
token_type: typeof data.token_type === "string" ? data.token_type : undefined,
expires_in: typeof data.expires_in === "number" ? data.expires_in : undefined,
scope: typeof data.scope === "string" ? data.scope : undefined
};
}
// ../oauth-helper/src/jwt.ts
function decodeJwtPayload(jwtToken) {
const parts = jwtToken.split(".");
if (parts.length !== 3) {
throw new OAuthHelperError(`Access token is not a JWT (expected 3 parts, got ${parts.length})`, "malformed_jwt");
}
try {
const json = atob(parts[1].replace(/-/g, "+").replace(/_/g, "/"));
return JSON.parse(json);
} catch (err) {
throw new OAuthHelperError(`Could not decode JWT payload: ${err instanceof Error ? err.message : String(err)}`, "malformed_jwt", { cause: err });
}
}
function enforceIssuer(payload, expectedIssuer) {
const iss = payload.iss;
if (typeof iss !== "string" || iss.length === 0) {
throw new OAuthHelperError(`Access token has no \`iss\` claim — refusing to trust an unsigned-issuer token.`, "issuer_mismatch");
}
if (iss !== expectedIssuer) {
throw new OAuthHelperError(`Access token issuer mismatch: discovered \`${expectedIssuer}\`, token claims \`${iss}\`.`, "issuer_mismatch");
}
}
// src/mcp.ts
class AtlasMcpError extends Error {
code;
constructor(message, code, options) {
super(message, options);
this.name = "AtlasMcpError";
this.code = code;
}
}
async function liftHelper(fn) {
try {
return await fn();
} catch (err) {
if (err instanceof OAuthHelperError) {
throw new AtlasMcpError(err.message, err.code, { cause: err });
}
throw err;
}
}
function liftHelperSync(fn) {
try {
return fn();
} catch (err) {
if (err instanceof OAuthHelperError) {
throw new AtlasMcpError(err.message, err.code, { cause: err });
}
throw err;
}
}
var DEFAULT_SCOPES = ["mcp:read", "offline_access"];
var WORKSPACE_CLAIM = "https://atlas.useatlas.dev/workspace_id";
var WORKSPACES_CLAIM = "https://atlas.useatlas.dev/workspace_ids";
async function beginConnect(options) {
const apiUrl = stripTrailingSlashes2(options.apiUrl);
liftHelperSync(() => validateIssuerUrl(apiUrl));
const fetchImpl = options.fetchImpl ?? fetch;
const randomBytesImpl = options.randomBytesImpl;
const scopes = options.scopes ?? DEFAULT_SCOPES;
return liftHelper(async () => {
const metadata = await discover(apiUrl, { fetchImpl });
const state = generateState({ randomBytesImpl });
const { codeVerifier, codeChallenge } = await generatePkce({ randomBytesImpl });
const clientId = await register(metadata, {
redirectUri: options.redirectUri,
clientName: options.clientName,
scopes
}, { fetchImpl });
const authorizationUrl = buildAuthorizationUrl({
authorizationEndpoint: metadata.authorization_endpoint,
clientId,
redirectUri: options.redirectUri,
state,
codeChallenge,
scopes
});
return {
authorizationUrl,
state,
codeVerifier,
clientId,
tokenEndpoint: metadata.token_endpoint,
issuer: metadata.issuer
};
});
}
async function completeConnect(options) {
if (options.state !== options.expectedState) {
throw new AtlasMcpError(`OAuth state mismatch — possible CSRF. Got \`${options.state}\`, expected the value returned from beginConnect.`, "callback_state_mismatch");
}
if (!options.code) {
throw new AtlasMcpError(`Authorization callback was missing the \`code\` parameter.`, "callback_missing_code");
}
const apiUrl = stripTrailingSlashes2(options.apiUrl);
liftHelperSync(() => validateIssuerUrl(apiUrl));
const fetchImpl = options.fetchImpl ?? fetch;
return liftHelper(async () => {
let tokenEndpoint = options.tokenEndpoint;
let issuer = options.issuer;
if (!tokenEndpoint || !issuer) {
const metadata = await discover(apiUrl, { fetchImpl });
tokenEndpoint = tokenEndpoint ?? metadata.token_endpoint;
issuer = issuer ?? metadata.issuer;
}
validateTokenEndpoint(tokenEndpoint);
const tokenResponse = await exchangeCode({
tokenEndpoint,
clientId: options.clientId,
redirectUri: options.redirectUri,
code: options.code,
codeVerifier: options.codeVerifier
}, { fetchImpl });
const claims = decodeJwtPayload(tokenResponse.access_token);
enforceIssuer(claims, issuer);
const workspaceId = extractWorkspaceClaim(claims);
const workspaces = extractWorkspacesClaim(claims);
const expiresIn = typeof tokenResponse.expires_in === "number" && tokenResponse.expires_in > 0 ? tokenResponse.expires_in : 3600;
return {
accessToken: tokenResponse.access_token,
refreshToken: tokenResponse.refresh_token ?? null,
expiresAt: Date.now() + expiresIn * 1000,
workspaceId,
workspaces
};
});
}
var SERVER_NAME_DEFAULT = "atlas";
function buildConfig(options) {
const apiUrl = stripTrailingSlashes2(options.apiUrl);
const url = `${apiUrl}/mcp/${encodeURIComponent(options.workspaceId)}/sse`;
const isMultiWorkspace = (options.workspaces?.length ?? 0) > 0;
if (isMultiWorkspace && !options.workspaces.includes(options.workspaceId)) {
throw new AtlasMcpError(`buildConfig: workspaceId \`${options.workspaceId}\` is not in the granted workspaces list — pass a default that is one of: ${options.workspaces.join(", ")}.`, "workspace_not_in_grant_list");
}
const block = isMultiWorkspace ? {
url,
headers: { Authorization: `Bearer ${options.accessToken}` },
env: { ATLAS_DEFAULT_WORKSPACE: options.workspaceId }
} : {
url,
headers: { Authorization: `Bearer ${options.accessToken}` }
};
const name = options.serverName ?? SERVER_NAME_DEFAULT;
switch (options.client) {
case "generic":
return isMultiWorkspace ? {
kind: "bare",
url: block.url,
headers: block.headers,
env: block.env
} : { kind: "bare", url: block.url, headers: block.headers };
case "claude-desktop":
case "cursor":
case "continue":
case "chatgpt":
return { kind: "wrapped", mcpServers: { [name]: block } };
default:
return assertNever(options.client);
}
}
function assertNever(x) {
throw new Error(`Unhandled MCP client id: ${String(x)}`);
}
async function connectMachineToMachine(_options) {
throw new AtlasMcpError("client_credentials grant is not yet enabled on the Atlas OAuth provider. Use the authorization-code flow (beginConnect / completeConnect) for now.", "grant_not_supported");
}
function stripTrailingSlashes2(s) {
let i = s.length;
while (i > 0 && s[i - 1] === "/")
i--;
return i === s.length ? s : s.slice(0, i);
}
function extractWorkspaceClaim(payload) {
const claim = payload[WORKSPACE_CLAIM];
if (typeof claim !== "string" || claim.length === 0) {
throw new AtlasMcpError(`Access token is missing the ${WORKSPACE_CLAIM} claim — was the token issued for an MCP scope?`, "missing_workspace_claim");
}
return claim;
}
function extractWorkspacesClaim(payload) {
const raw = payload[WORKSPACES_CLAIM];
if (raw === undefined)
return [];
if (!Array.isArray(raw)) {
console.warn(`[@useatlas/sdk] ${WORKSPACES_CLAIM} claim was present but not an array (got ${typeof raw}); falling back to single-workspace.`);
return [];
}
const filtered = raw.filter((entry) => typeof entry === "string" && entry.length > 0);
if (filtered.length !== raw.length) {
const dropped = raw.length - filtered.length;
console.warn(`[@useatlas/sdk] ${WORKSPACES_CLAIM} claim contained ${dropped} non-string or empty entr${dropped === 1 ? "y" : "ies"}; using only the ${filtered.length} valid entr${filtered.length === 1 ? "y" : "ies"}.`);
}
return filtered;
}
// src/client.ts

@@ -492,2 +913,24 @@ import { isChatErrorCode, isRetryableError } from "@useatlas/types";

return res;
},
mcp: {
async beginConnect(opts) {
return beginConnect({ apiUrl: base, ...opts });
},
async completeConnect(opts) {
return completeConnect({ apiUrl: base, ...opts });
},
buildConfig(opts) {
return buildConfig({ apiUrl: base, ...opts });
},
async connectMachineToMachine(opts) {
return connectMachineToMachine({ apiUrl: base, ...opts });
},
async listAgents() {
const res = await get("/api/v1/me/oauth-clients");
return unwrap(res);
},
async revokeAgent(clientId) {
const res = await post(`/api/v1/me/oauth-clients/${encodeURIComponent(clientId)}/revoke`, {});
return unwrap(res);
}
}

@@ -552,3 +995,8 @@ };

createAtlasClient,
connectMachineToMachine,
completeConnect,
buildConfig,
beginConnect,
AtlasMcpError,
AtlasError
};
+12
-4
{
"name": "@useatlas/sdk",
"version": "0.0.12",
"version": "0.0.14",
"description": "TypeScript SDK for the Atlas text-to-SQL agent API",
"type": "module",
"scripts": {
"build": "rm -rf dist && bun build src/index.ts src/client.ts --outdir dist --target node --packages external && bun x tsc -p tsconfig.build.json",
"build": "rm -rf dist && bun build src/index.ts src/client.ts src/mcp.ts --outdir dist --target node --external '@useatlas/types' && bun x tsc -p tsconfig.build.json",
"prepublishOnly": "bun run build",
"test": "bun test src/__tests__/client.test.ts src/__tests__/stream.test.ts src/__tests__/integration.test.ts src/__tests__/stream-integration.test.ts src/__tests__/fetch-starter-prompts.test.ts"
"test": "bun test src/__tests__/client.test.ts src/__tests__/stream.test.ts src/__tests__/integration.test.ts src/__tests__/stream-integration.test.ts src/__tests__/fetch-starter-prompts.test.ts src/__tests__/mcp.test.ts"
},

@@ -21,2 +21,7 @@ "exports": {

"default": "./dist/client.js"
},
"./mcp": {
"types": "./dist/mcp.d.ts",
"import": "./dist/mcp.js",
"default": "./dist/mcp.js"
}

@@ -48,4 +53,7 @@ },

"dependencies": {
"@useatlas/types": "^0.0.16"
"@useatlas/types": "^0.0.25"
},
"devDependencies": {
"@atlas/oauth-helper": "workspace:*"
}
}

@@ -206,2 +206,41 @@ # @useatlas/sdk

### Context Warnings (Degraded Answer Signal)
The chat route emits `data-context-warning` frames whenever the agent ran
past a non-fatal degradation — the org semantic layer or learned-patterns
lookup failed, or the workspace is approaching its plan budget — and the
run was allowed to proceed against reduced context. These are not stream
events from `streamQuery()`; they arrive on the AI-SDK UI Message Stream
channel that `chat()` returns. If you are routing a raw `Response` from
`atlas.chat()` through your own SSE consumer, parse each
`data-context-warning` frame body with `parseContextWarning` from
`@useatlas/types`:
```typescript
import { parseContextWarning, type ChatContextWarning } from "@useatlas/types";
// Inside your SSE frame handler:
if (frame.type === "data-context-warning") {
const warning: ChatContextWarning | null = parseContextWarning(frame.data);
if (warning) {
console.warn(`[${warning.code}]`, warning.title, warning.detail ?? "");
// Surface a per-message warning banner — the answer is still usable
// but was generated against reduced context. Do NOT treat it as a
// hard error.
} else {
// Log on null so a future server-side wire-shape change (unknown
// code, missing title, wrong severity literal) is observable in dev
// rather than silently dropped. In production, gate this behind a
// one-shot ref so a runaway stream of malformed frames doesn't
// flood the console — see the in-tree `useContextWarnings` hook for
// an example dedup pattern.
console.warn("[atlas-sdk] dropped malformed data-context-warning frame", frame.data);
}
}
```
The frame's `severity: "warning"` discriminator separates these from
`data-error` frames, so a single SSE consumer can route both without
misclassifying a degraded answer as a failure.
## Error Handling

@@ -208,0 +247,0 @@