Organizations, API keys and agents
7. Organizations, API keys and agents
7.1 The organization model
There are no users, passwords or teams. An organization owns channels, posts, media, notifications and API keys. The API key identifies the organization on every request. A deployment can have several organizations; each key sees only its own rows.
rustiz init --name "Acme" # name used only when it creates the first org
rustiz org create "Acme EU" # prints organization id + API key
rustiz org list # id<TAB>name, one per line
rustiz org rotate-key <org-id> # prints the new key
rustiz org ... prints plain text, not JSON. rotate-key also revokes every
MCP OAuth access/refresh token issued to that organization; clients must
re-authorize or be reconfigured with the new key. Channel credentials are not
deleted by a key rotation.
rustiz init is idempotent: it keeps an existing JWT_SECRET, keeps the
stored API key if it still matches an organization, and otherwise adopts the
first existing organization rather than creating a second one and writes that
organization’s key to config.env. It only errors if the adopted organization
has no API key at all (then rustiz org rotate-key <id> is the fix).
7.2 Authentication
Send the key as either:
Authorization: Bearer <api-key>
or the raw value (what Postiz sends):
Authorization: <api-key>
/mcp/{key} does not exist in this port. Keys never travel in URLs or logs.
The server hashes MCP OAuth tokens with SHA-256 and compares the hash; API keys
are stored in the organization row.
7.3 MCP over stdio (local agent)
rustiz init prints the exact block. It looks like:
{
"mcpServers": {
"rustiz": {
"command": "/usr/local/bin/rustiz",
"args": ["mcp"]
}
}
}
The key is read from RUSTIZ_CONFIG/~/.rustiz/config.env; no secret appears
in the snippet. rustiz mcp also starts the scheduler and, for a loopback
HTTP FRONTEND_URL, the login callback listener. You can override the key:
rustiz mcp --api-key <key>
stdout carries the MCP protocol; diagnostics go to stderr. A service manager that captures stdout must not inject banners into it.
If the machine has no config, rustiz mcp fails with a message pointing at
rustiz init rather than complaining about JWT_SECRET.
7.4 MCP over HTTP (remote agent)
Endpoint: ${FRONTEND_URL}/mcp.
Bearer API key (simplest):
{
"mcpServers": {
"rustiz": {
"type": "http",
"url": "https://social.example.com/mcp",
"headers": {
"Authorization": "Bearer <api-key>"
}
}
}
}
The transport is stateless Streamable HTTP. Tool calls normally return JSON;
subscriptions/listen uses an event stream. Host must be in the allowlist:
FRONTEND_URL’s host (and host:port), localhost, 127.0.0.1, plus anything
in MCP_ALLOWED_HOSTS.
OAuth 2.1 + PKCE (for clients that support it): omit the Authorization
header. A request without credentials gets 401 with:
WWW-Authenticate: Bearer resource_metadata="https://social.example.com/.well-known/oauth-protected-resource/mcp"
Discovery and endpoints:
| Endpoint | Purpose |
|---|---|
GET /.well-known/oauth-protected-resource and .../mcp | RFC 9728 resource metadata (resource, authorization_servers, bearer_methods_supported, scopes_supported: ["rustiz"]). |
GET /.well-known/oauth-authorization-server | RFC 8414 authorization-server metadata. |
GET/POST /oauth/authorize | Consent page. It asks for the organization API key; there is no user login. |
POST /oauth/token | Authorization-code and refresh-token grants, token_endpoint_auth_method: none. |
POST /oauth/register | RFC 7591 dynamic client registration (for older clients). |
Details an operator may need:
- Only
response_type=code,S256PKCE and a code challenge of at least 43 characters are accepted.resourcemust be${FRONTEND_URL}/mcpor${FRONTEND_URL}. - Client ID Metadata Documents are supported: a client id that is an HTTPS URL
(or a plain-HTTP URL when
ALLOW_PRIVATE_NETWORK=true) is fetched through the SSRF-safe client, max 64 KiB, and must contain a matchingclient_idandredirect_uris. - Registration accepts
httpsURIs andhttponly for loopback hosts (localhost,127.0.0.1,[::1]); at authorization the requestedredirect_urimust match a registered one exactly, except that a loopback redirect may use any port (RFC 8252).javascript:,data:,file:,blob:,ftp:,ws:/wss:and fragments are rejected. At most 10 URIs, 2048 bytes each. - Authorization codes live 5 minutes and are single-use. Access tokens live 1 hour; refresh tokens live 30 days and are rotated on use.
- The consent form is the only HTML the server renders besides the channel
connect result page. It is protected by
X-Frame-Options: DENY, a restrictive CSP,Referrer-Policy: no-referrerandCache-Control: no-store. - Registering a client grants nothing until someone enters a valid API key on the consent page.
- Revoke everything for an organization by rotating its API key.
7.5 Connecting a channel from an agent
REST and MCP share one code path.
rustiz channels list
rustiz providers --status
rustiz channels connect x --open
rustiz channels connect mastodon-custom --external-url https://mastodon.social
rustiz channels connect bluesky --fields ./bluesky.json
rustiz channels connect telegram
rustiz channels connect-status <connectionId>
--fields takes a JSON file or - for stdin. --timeout defaults to 300
seconds for the in-process OAuth wait.
MCP connectChannelTool arguments:
| Argument | Use |
|---|---|
provider | Provider identifier. Not needed when polling a connectionId. |
connectionId | Poll a Telegram/Moltbook/Farcaster/OAuth handshake started earlier. Safe to repeat. |
externalUrl | Self-hosted instance URL (required for mastodon-custom). |
customFields | Credential object for customFields providers. Telegram also accepts {"chatId":"-100..."}, Moltbook {"apiKey":"..."}, Farcaster {"signerUuid":"..."}. |
name | Moltbook agent name to register. |
description | Moltbook agent description. |
If customFields is present and is a JSON object, it takes precedence over the
OAuth flow for any provider. Only send it to providers whose
integrationSchema lists customFields.
Special flows:
- Telegram (
telegramBot): with nocustomFields, the server returnsconnectionId, awordand a/connect <word>command. A human adds the bot to the target group/channel (ideally as admin), sends the command, then the agent polls with the sameconnectionId. WithcustomFields.chatIdit connects immediately. - Moltbook (
moltbookClaim): the server registers an agent and returnsclaimUrl(and possibly a verification code). The human claims it, then the agent polls. - Farcaster (
signer): the server creates a Neynar signer and returnsapprovalUrl/qrCode; the human approves in the Farcaster app, then the agent polls. WithcustomFields.signerUuidof an already-approved signer it connects immediately. - Between-steps channels (
is_between_steps: Facebook pages, Instagram accounts, LinkedIn companies, YouTube channels, Google Business locations, Tumblr blogs): the OAuth login creates a channel withinBetweenSteps=true. List the choices withtriggerTooland finish withchannelSetupTool:
{
"integrationId": "<id>",
"methodName": "pages",
"dataSchema": []
}
{
"integrationId": "<id>",
"data": { "page": "<page-id>" }
}
triggerTool only allows function names declared in the provider’s
integrationSchema (functions array); it refreshes an expired token once and
disconnects on a hard auth error. channelSetupTool rejects a channel that is
already set up and reports That page is already connected as another channel
on a uniqueness conflict.
OAuth providers need a human to open the URL. MCP clients that declare URL
elicitation receive the link through the multi-round-trip flow; other clients
get {connected:false, pending:true, connectionId, url, message} and should
poll with connectionId or re-check integrationList.
7.6 Posting workflow
Typical tool order:
providersStatusTool
integrationList
integrationSchema { platform }
connectChannelTool (if needed)
validatePostTool
schedulePostTool
postStatusTool
validatePostToolchecks channel state (disabled, refresh needed, between steps), providervalidate_settings, character count (HTML stripped, counted in characters), thread support andcheck_validitymedia rules. It does not create anything.schedulePostToolacceptssocialPost[]withintegrationId,date(UTC),type(draft/schedule/now), optionalshortLink,settingsas[{key,value}]andpostsAndComments([0]is the post, the rest are replies).attachmentsmay be a URL string or an object withpath,alt,thumbnail,thumbnailTimestamp.- Resends are safe: an identical call within 10 minutes, or the same
idempotencyKeywithin 24 hours, returns the first result. Reusing a key with different arguments is rejected. An in-flight or abandoned claim tells you to checkpostsListToolbefore retrying with a new key. - With the MCP Tasks extension,
type:"now"returns a durable task handle; otherwise pollpostStatusTool. Task state is derived from posts and survives restarts. subscriptions/listenonrustiz://posts/<postId>orrustiz://groups/<groupId>sends a hint on state change; it does not carry content or errors.