npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@hawkenai/atrium-sdk

v0.3.0

Published

Browser AtriumClient, server AtriumAdmin, and verifyAtriumMcpJwt for partner MCP auth. Mint tokens on your server; the browser client only holds a short-lived user session.

Downloads

735

Readme

Atrium client SDK

Headless Atrium API client for partners who already have their own users and UI.

The browser never holds the org secret. Your server mints a short-lived per-user token. This client attaches it to every Atrium call and refreshes it.

npm install @hawkenai/atrium-sdk

Partner setup

  1. In Atrium: Settings → Developer. Generate the org API key (hawko_). Keep it on your server only.
  2. Add your app origins (scheme + host + port). Example: https://app.partner.com.
  3. For partner tools, set the MCP URL and a separate MCP signing secret on the same page.
  4. Add one endpoint behind your login:
GET /api/atrium-token
→ { "token": "<session>", "expiresIn": 3600 }

That endpoint calls Hawken:

POST /api/atrium/partner/sessions
Authorization: Bearer <hawko_ org key>
Content-Type: application/json

{
  "externalUserId": "<your user id>",
  "name": "Ada Lovelace",
  "email": "[email protected]"
}

If that email already belongs to a user in this org, that user is reused and linked. 409 EMAIL_ALREADY_EXISTS only when the email belongs to another organization.

  1. In the browser:
import { AtriumClient } from '@hawkenai/atrium-sdk'

const atrium = new AtriumClient({
	tokenUrl: '/api/atrium-token',
})

apiBase is optional and defaults to https://platform.hawken.ai. Pass it for local or staging (http://127.0.0.1:3001). tokenUrl is called with credentials: 'include' so your session cookie is sent. Use getToken instead if you mint another way.

Do not pass the org key (hawko_), a hawk_ API key, or a legacy atk_ secret into AtriumClient. Those stay on your server. Use AtriumAdmin there.

Admin (server only)

AtriumAdmin holds the org API key (hawko_) and calls the same /api/atrium/* URLs. Legacy atk_ keys still work. It can provision teams, members, and projects without being the team owner. Chat, conversations, files, and personal settings still require a minted user session.

import { AtriumAdmin, AtriumClient } from '@hawkenai/atrium-sdk'

const admin = new AtriumAdmin({ secret: process.env.ATRIUM_ORG_SECRET })
const { user, token } = await admin.sessions.create({
	externalUserId,
	name,
	email,
})
await admin.teams.addMember(teamId, { userId: user.id })

Creating a team or project with the org key requires ownerUserId (the Hawken user id from mint).

| Method | HTTP | Returns | |---|---|---| | sessions.create(body) | POST /api/atrium/partner/sessions | { token, expiresIn, user } | | orgMembers.list() | GET /api/atrium/org-members | { members } | | teams.list() | GET /api/atrium/teams | { teams } (all org teams) | | teams.get(id) | GET /api/atrium/teams/:id | { team } | | teams.create(body) | POST /api/atrium/teams | { team } | | teams.update(id, body) | PATCH /api/atrium/teams/:id | { team } | | teams.members.list(id) / teams.addMember / teams.removeMember | members CRUD | { members } / { member, team } / { team } | | teams.settings.get(id) / teams.settings.update(id, body) | team settings | { team, settings, canEdit? } | | projects.list() | GET /api/atrium/projects | { projects } (all non-hidden) | | projects.get(id) | GET /api/atrium/projects/:id | { project } | | projects.create(body) | POST /api/atrium/projects | { project } | | projects.update(id, body) | PATCH /api/atrium/projects/:id | { project } |

Allowed origins

If the org allowlist is empty, any browser origin may call Atrium (the minted token is still required).

If the allowlist is set, browser calls must send an Origin that is on the list. The Hawken web app is always allowed. Server-side calls with no Origin header are allowed.

Add both http://127.0.0.1:<port> and http://localhost:<port> in development. Browsers treat those as different origins.

Client methods

Request and response shapes live in atrium-client.d.ts (TypeScript picks them up from the package). Conversation title is optional; the backend names the chat from the first user turn.

const { conversation } = await atrium.conversations.create()
const stream = atrium.chat.stream({
	model: 'openai/gpt-4o-mini',
	conversationId: conversation.id,
	messages: [{ role: 'user', content: 'Hello' }],
})

Nested names and flat names call the same methods.

| Method | HTTP | Returns | |---|---|---| | conversations.list() / listConversations() | GET /api/atrium/conversations | { conversations } | | conversations.create(body?) / createConversation(body?) | POST /api/atrium/conversations | { conversation } | | conversations.messages.list(id) / listMessages(id) | GET /api/atrium/conversations/:id/messages | { messages } | | conversations.messages.create(id, body) / appendChatTurn(id, body) | POST /api/atrium/conversations/:id/messages | { messages } | | chat.stream(body, options?) / streamChat(body, options?) | POST /api/atrium/chat/stream | async iterator of SSE frames (delta is token text) | | getLlmSpecs() | GET /api/atrium/llm-specs | { results } | | teams.list() / listTeams() | GET /api/atrium/teams | { teams } | | teams.create(body) / createTeam(body) | POST /api/atrium/teams | { team } | | teams.update(id, body) / updateTeam(id, body) | PATCH /api/atrium/teams/:id | { team } | | projects.list() / listProjects() | GET /api/atrium/projects | { projects } | | projects.create(body) / createProject(body) | POST /api/atrium/projects | { project } | | projects.update(id, body) / updateProject(id, body) | PATCH /api/atrium/projects/:id | { project } | | files.list() / listLibraryFiles() | GET /api/atrium/library/files | { files } | | files.upload(files) / uploadLibraryFiles(files) | POST /api/atrium/library/files | { files } | | files.download(id) / downloadFile(id) | GET /api/atrium/files/:id/download | Blob |

Also on the client: ensureToken(), request(), requestJson(). Those are for advanced use. request only allows /api/atrium/*.

Partner tools (MCP)

These instructions are for you (the partner), not Hawken internals.

Atrium runs the agent loop. For tools that hit your data or APIs, host an MCP server on your Node process at /api/atrium-mcp/*. Atrium calls that URL during the turn. Do not send tools on chat.stream today — Atrium overwrites that field with its artifact tools.

Use MCP for server work (get_invoice, search_matters). Keep UI actions (open_invoice, pick a row) in your app, not on MCP.

When Atrium hits your MCP

Atrium does not call your tools on every token or every chat message.

| When | What Atrium does | |---|---| | Turn start (chat.stream) | tools/list once (discover schemas). Cache this per org / URL / tool version. Not an index crawl. | | Model chooses a partner tool | tools/call for that tool only | | Later turns in the same chat | Reuse cached defs unless you change tools (then we list again) |

Unused tool definitions still go to the model each turn you enable MCP. Unused tools are not executed. Keep the list small.

Host it on the same server

Add Streamable HTTP next to your existing /api/atrium-token route. Same deploy. Atrium (Hawken’s servers) must be able to reach it — localhost only works on your machine.

npm install @modelcontextprotocol/sdk zod
import express from 'express'
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'
import { z } from 'zod'

const app = express()
app.use(express.json())

function createPartnerMcpServer(user: { id: string }) {
	const server = new McpServer({ name: 'partner-atrium-tools', version: '1.0.0' })

	server.tool(
		'get_invoice',
		'Load one invoice the current user may see.',
		{ invoiceId: z.string() },
		async ({ invoiceId }) => {
			const invoice = await loadInvoiceForUser(user.id, invoiceId)
			return {
				content: [{ type: 'text', text: JSON.stringify(invoice) }],
			}
		}
	)

	return server
}

app.all('/api/atrium-mcp', async (req, res) => {
	const user = await requirePartnerUserFromBearer(req.headers.authorization)
	if (!user) {
		res.status(401).json({
			jsonrpc: '2.0',
			id: null,
			error: { code: -32001, message: 'Unauthorized' },
		})
		return
	}

	const server = createPartnerMcpServer(user)
	const transport = new StreamableHTTPServerTransport({
		sessionIdGenerator: undefined,
	})
	await server.connect(transport)
	await transport.handleRequest(req, res, req.body)
})

sessionIdGenerator: undefined is stateless (one request, no in-memory session). Prefer that behind a load balancer. Pass req.body if Express already parsed JSON.

Package names in the MCP TypeScript SDK move. If those imports fail, use the current Streamable HTTP + Express example from the MCP TypeScript SDK.

Auth and identity

Atrium is the HTTP client (the agent runs on Hawken’s servers, not in the partner browser). It does not send hawko_, a hawk_ key, the user’s browser cookie, or the minted Atrium user session (that token is only for the browser/SDK calling Atrium).

Atrium sends a short-lived JWT it signed:

POST /api/atrium-mcp
Authorization: Bearer <atrium-mcp-jwt>
Content-Type: application/json

Intended claims (same externalUserId you sent at mint):

{
  "iss": "https://platform.hawken.ai",
  "aud": "https://app.partner.com/api/atrium-mcp",
  "exp": 1710003600,
  "orgId": "<hawken org id>",
  "userId": "<hawken user id>",
  "externalUserId": "<your user id>"
}

TTL is minutes, not hours. One JWT per MCP request (list or call).

How you verify: store an MCP signing secret in Atrium Settings → Developer (separate from hawko_). Atrium HS256-signs the JWT with that secret. Verify with verifyAtriumMcpJwt from @hawkenai/atrium-sdk/mcp — not on AtriumAdmin. Then load your user from externalUserId. Reject if the secret is missing or the JWT fails.

import { verifyAtriumMcpJwt } from '@hawkenai/atrium-sdk/mcp'

function requirePartnerUserFromBearer(authorization: string | undefined) {
	const claims = verifyAtriumMcpJwt(authorization, {
		secret: process.env.ATRIUM_MCP_SIGNING_SECRET!,
		audience: 'https://app.partner.com/api/atrium-mcp',
	})
	return loadUserByExternalId(claims.externalUserId)
}

Do not use hawko_ as that HMAC. If it leaks, an attacker can mint users and call your tools.

Run every tool as that user. Do not use a god-mode DB key. HTTPS in production.

What to register

  • A small tool list. Unused defs cost tokens every turn.
  • Stable names. Do not collide with Atrium names: create_artifact, update_artifact, read_artifact, list_library, run_code.
  • Fast, idempotent handlers. Atrium may retry if a stream drops.
  • Public base URL, e.g. https://app.partner.com/api/atrium-mcp. Save that URL and the signing secret in Atrium Settings → Developer.

If the model always needs the same small blob, load it on your server and put it in messages before chat.stream. That is context, not tools.

Local demo

From atrium-sdk-partner-demo:

./scripts/start.sh

App: http://127.0.0.1:3040 (or http://localhost:3040). Tests: node scripts/test.mjs.

License

MIT. See LICENSE. Using Atrium still requires a Hawken account and an Atrium license.