Studio Backend

Integrations and External Services

This page covers two things: the integrations a project configures for its agents — webhooks around a conversation and MCP tools during it — and the external services the platform itself depends on.

Project Integrations

Integrations are stored in the project document and executed by the engine. The Studio, the AI Assistant and the MCP server all manage them through ProjectRestController:

Kind Stored in Create / edit Executed

pre

webhookSpecifications (PreSessionWebhook)

POST/PUT /api/v1/project/{id}/integrations/pre

At the start of a session; the result is given to the model (relational agents use it in their planning prompts).

during

mcpServerSpecifications

POST/PUT .../integrations/during

During the conversation, as tools of the agent.

post

webhookSpecifications (PostSessionWebhook)

POST/PUT .../integrations/post

After the session ended, for example to deliver the transcript.

connector

mcpConnectorSpecifications

POST/PUT .../integrations/connector

During the conversation, as tools of the agent, with the end user’s own connection to the service.

GET .../integrations returns the four lists, and every row carries its type. Deleting is one endpoint for all kinds, DELETE .../integrations?type={pre|during|post|connector}&ref={ref}, so the kind must be known to delete a row. Code that opens, edits or deletes an integration by ref has to branch on its kind first — the pre/post and the MCP-server edit views each look the ref up only in their own list, and a ref routed to the wrong one never resolves.

Webhooks

A webhook has a name, a method (GET or POST), a URL, headers, enabled, a timeout in milliseconds (default 5000) and a number of retries (default 0). A pre-session webhook also has an llmDescription, which tells the model what the returned data means, and a default value. Header values typically hold credentials; the MCP server masks them in its results.

MCP Servers

An MCP server integration names a URL, a transport (StreamableHttp, the default, or SSE), an optional authentication header and the tools the agent may use. When it is saved, McpServerService connects to the server as an MCP client, initializes the session (timeout 20 seconds, adjustable with the PROMETHIST_MCP_REQUEST_TIMEOUT_SECONDS environment variable), lists its tools and stores the selected ones as full tool specifications. A Streamable HTTP URL without a path is called at /mcp.

GET /api/v1/integration/mcp/load-tools runs the same probe without saving anything; the Studio’s Try connection button and the MCP tool load_mcp_tools use it.

The selection is intersected with what the server advertises. An empty selection therefore stores no tools, and an integration without tools gives the agent nothing at conversation time — the most common reason an agent "does not call" an integrated tool.

MCP Connectors

Connectors are MCP servers from a global catalog (McpConnector in promethist-common, collection mcpConnector), each with a name, description, icon, the server URL and its tool list, and an OAuth client registration. The catalog is maintained on the engine side, which also refreshes each connector’s tool list and keeps the end users' connections to the connected service.

A project enables a connector by its ref with a subset of the catalog’s tools (McpConnectorIntegrationService); an unknown tool name is rejected. GET .../integrations/connectors merges the catalog with the project’s state: each entry says whether it is enabled and which tools are selected, and a connector that is enabled but no longer in the catalog shows up with its ref as its name and should be removed.

Referencing Integrations from Agents

Global integrations are available to every agent of the project. The tools of a non-global MCP server or connector are served only to agents whose instructions name each tool, as @MCP:{ref}_{toolName}; (the match is case-insensitive and the trailing semicolon is required). The whole-integration form @MCP:{ref}; records a dependency but exposes no tools on its own.

The ref of an MCP-server integration is derived from its name when it is saved, so renaming one can change its ref and break the references to it; webhook and connector refs stay stable.

Identity Providers

Projects that admit end users through their own identity providers name Keycloak identity-provider aliases (Identity Providers and External Roles). KeycloakIdentityProviderProbe checks a new alias against the realm the backend trusts without admin credentials: it reads the realm’s discovery document and then calls the broker endpoint {realm}/broker/{alias}/endpoint, which answers 404 only for an unknown alias. An unreachable Keycloak yields "unknown" rather than a verdict, and the save goes through. Probing a known OIDC alias leaves a failed identity-provider login event in Keycloak.

Secrets

Secret Handling

LiveAvatar API key

Stored per account or per project in the secret collection (SecretRestController), never inside the project document. The visual endpoints use it to fetch live-avatar previews from the LiveAvatar API (LIVE_AVATAR_API_URL); a response header tells the Studio when a key is missing.

Vault values

VaultService reads named values from Azure Key Vault (promethist.vault.impl=azure) or from nothing (empty, the default). Only the staff endpoint GET /api/vault/{name} exposes it.

Re-encoding of Uploaded Videos

A video uploaded to an interactive-content element — in the Studio (POST /api/v1/project/multimodality-video) or through the MCP tool upload_interactive_content_file — is stored as uploaded at platform/project/{id}/multimodalInteraction/{ref}.mp4 and served from there right away, whatever its container. ProjectService then queues its re-encoding with VideoTranscoder: the original is downloaded into a temporary directory, re-encoded with ffmpeg, and the copy stored next to it as {ref}_encoded_v{n}.mp4, n being ENCODE_VERSION. The element’s url is then switched to the copy by a compare-and-set that matches only while the element still serves that very upload, so an edit, a newer upload or a removal made in the meantime wins. The copy is then deleted again, unless an element serves it after all — a form saved with the address of the copy an earlier upload made. An element missing at that point is looked up once more a moment later, as an edit replaces an element by removing it and adding its new version. The copy’s address keeps the original’s q query: when a form opened before the switch sends the original’s address back, the update recognizes the same upload and keeps the copy.

Part Encoding

Video

H.264 High, 8-bit 4:2:0 (libx264, preset veryfast), at the original’s video bitrate but at most 3 Mbit/s; the longer edge downscaled to at most 1920 pixels, never upscaled; the frame rate kept up to 60 frames a second (a variable one made constant) and a phone video’s rotation applied. HDR is converted to 8-bit without tone mapping.

Audio

AAC-LC, stereo, 128 kbit/s (less at a low sample rate).

Container

MP4 with the index first (+faststart), without subtitles, data streams, chapters or metadata.

The original’s video bitrate is that of its video stream. A container that stores none per stream (WebM, Matroska) gives the file’s bitrate less the audio’s, and a file without a duration — a browser’s recording — gives its size over the time of its last frame, which a second ffprobe pass reads; when none of that is known, the copy gets 3 Mbit/s. A browser’s recording also declares 1000 frames a second; the cap of 60 keeps it from being encoded at that rate. The decoder buffer (-maxrate, -bufsize) is sized for at least 200 kbit/s, so that a keyframe of a low-bitrate video still fits it. A copy can come out larger than its original, for one because the audio is always stereo, and it replaces the original all the same.

These settings are constants of VideoTranscoder, and changing any of them means bumping ENCODE_VERSION, so that copies made with other settings stay recognizable by name. The originals are kept for the same reason: a copy can always be made again from its original. Only new uploads are re-encoded — an element keeps serving whatever its url names, and a video given by URL is never re-encoded.

ffmpeg and ffprobe read only what an uploaded video needs: the containers MP4, MOV and 3GP, WebM and Matroska, and Ogg; the decoders of the common video, audio, subtitle and cover-art codecs (DECODERS); and pictures of at most 4096 × 2304 pixels, any 4K video. A file with anything else — an AVI, a QuickTime Animation track, an 8K or a 5.7K video — fails and keeps its original; a picture too large is refused before it is decoded.

ffmpeg runs as a child process in the backend’s container and shares the pod’s memory with the JVM, which shapes how it runs:

  • One job at a time, on a single worker thread where it may block, though every transfer and database call has a timeout; at most 20 more jobs wait, and a further one is dropped — its element keeps the original.

  • A newer upload to the same element, clearing its URL or removing it cancels the element’s job and kills its ffmpeg. Clearing and removing delete the original and every copy. Cloning copies the original and, when the element serves one, its copy; a clone left serving its copy of the original is re-encoded like an upload.

  • ffmpeg and ffprobe run with nice -n 10 and, where prlimit exists (it does in the backend image), with their memory capped at promethist.video-transcoding.memory-limit (768 MB): an encoding that needs more fails. ffmpeg has two decoder, scaler and encoder threads each, and is killed after promethist.video-transcoding.timeout (30 minutes). The startup log says which of these apply.

  • One encoding peaks at about 280 MB for a 1080p source and about 450 MB for a 4K one, which is what the backend’s memory above its heap is for (Helm Charts). Storing the copy holds it on the JVM heap while it is written, as storing an upload of the same size does.

A failure — no ffmpeg, a file ffmpeg cannot read or does not take, an ffmpeg error or timeout, a storage error — is logged as a warning and leaves the original served, as does a restart in the middle of a job. Without ffmpeg and ffprobe on the PATH, or with promethist.video-transcoding.enabled=false — as in issue environments — nothing is re-encoded (Video Re-encoding settings).

External Services of the Platform

Service How the platform uses it

Keycloak

Issuer of Studio users' access tokens (spring.security.oauth2.resourceserver.jwt.issuer-uri), sign-in of the Studio, realm checks of identity-provider aliases.

Azure OpenAI, OpenAI

The AI Assistant (AI Assistant), knowledge descriptions, the initial plans of relational agents, embeddings and transcription (Knowledge Processing, AI settings).

Azure AI Document Intelligence

Layout analysis of knowledge documents.

Stripe

Billing (Usage and Billing).

Mailgun

Invitation emails. MailgunService from promethist-common sends a stored template — promethist-platform: tenant invite or promethist-platform: project invite by default — with the variables url, tenant or project, and user (the inviting person). Changing the wording of an invitation is a change of the Mailgun template, not of the code. Without an API key the platform uses a no-op mail service.

Blob storage

ReactiveResourceStorage of promethist-common writes the account logo under platform/tenant/{id}/ and the project logo and the images and videos of interactive content under platform/project/{id}/ — an uploaded video both as uploaded and re-encoded (Re-encoding of Uploaded Videos); the files are public under https://repository.promethist.ai/public.

ffmpeg

Re-encoding of uploaded videos (Re-encoding of Uploaded Videos), installed in the backend image from the Ubuntu packages.

Promethist asset repository

The avatar and environment catalog (arc/bundles/default/index.json), cached in memory.

Bridge, engine

Test suites and live tests (Test Suites), voice previews.

Zendesk

The Studio’s contact form (/api/contact) creates a Zendesk ticket from the Next.js server.