Skip to content

MCP Integration

Strøm exposes a Model Context Protocol (MCP) endpoint that allows AI agents like Claude, Cursor, and other MCP-compatible tools to interact with your workflow orchestration server using a standardized interface.

The MCP endpoint provides a set of tools for AI agents to:

  • List and query available workspaces and tasks
  • Retrieve detailed task definitions and execution history
  • Execute tasks and monitor job status in real-time
  • View logs and troubleshoot failed steps
  • Cancel running or pending jobs

This enables AI agents to autonomously orchestrate complex workflows, answer questions about your infrastructure, and help debug issues.

Add the mcp section to your server-config.yaml:

mcp:
enabled: true

The MCP endpoint is served at http://localhost:8080/mcp using Streamable HTTP transport (JSON response mode). When auth is configured on the server, MCP clients must send Authorization: Bearer <token> headers with each request.

The MCP Streamable-HTTP transport validates the request Host header to prevent DNS-rebinding attacks, accepting loopback hosts by default. Strøm automatically allow-lists the host from auth.base_url, so a standard reverse-proxy deployment works out of the box (this is also why auth.base_url is required when mcp.enabled: true).

If the server is reachable under additional hostnames (an alias domain, a second ingress), list them explicitly — otherwise requests to those hosts are rejected before authentication with a disallowed Host header warning:

mcp:
enabled: true
# Extra hostnames beyond auth.base_url's host and loopback. Include a port
# only for non-default ports, e.g. "stroem.internal:8080".
allowed_hosts:
- alias.example.com
# Browser Origin allow-list. Leave empty (default) for non-browser clients
# like Claude Code; set it only when browser-based MCP clients connect.
allowed_origins: []

The MCP server provides eleven tools for interacting with Strøm:

Tool Description Parameters
list_workspaces List all workspaces and their task counts, with the server-config display_name / description when set none
list_tasks List tasks in a workspace workspace? (optional; lists all if omitted)
get_task Get detailed task definition workspace, task_name
execute_task Execute a task and return job ID workspace, task_name, input?
get_job_status Get current job status with step details job_id
get_job_logs The end of a job’s log (last 256 KiB), formatted job_id, step?, tail_bytes?
list_jobs List recent jobs with optional filters workspace?, task_name?, status?, limit?
cancel_job Cancel a running or pending job job_id
list_artifacts List a job’s artifacts (name, content type, size, download URL) job_id
get_artifact Fetch one artifact’s content job_id, name
create_upload_url Get a single-use URL to upload a file for a file input field workspace, task_name, file_name

When the log is longer than the tail, the result ends with [truncated: showing the last 256.0 KiB of up to 83.3 MiB — earlier lines omitted].

get_artifact returns content shaped to the artifact’s type: textual artifacts (text, JSON, YAML, XML) come back as inline text, image/* artifacts as image blocks the agent can view, and any other binary (PDF, archives, …) as a base64 blob resource. Artifacts larger than 1 MiB are not inlined — download them via the HTTP artifact API (the URL is in list_artifacts) instead.

A task can have file input fields. get_task shows them as type: file. There are two ways to give such a field a file with execute_task.

Inline, for small files. Put an object in input instead of a value. The decoded content must not be more than 1 MiB (input_files.mcp_inline_max_bytes):

Call: execute_task(
workspace="default",
task_name="import-report",
input={ "data_file": { "name": "report.csv", "content": "id,total\n1,10\n" } }
)

Use content_base64 instead of content for binary data. The object must have exactly the two keys name and content (or content_base64); another shape or invalid base64 gives Input field '…': the file cannot be used. A file over the limit gives Input field '…': inline content is larger than N bytes. The server stores the file as your upload (content type application/octet-stream) and then creates the job.

Upload URL, for larger files and for agents that have a shell:

Call: create_upload_url(workspace="default", task_name="import-report", file_name="report.csv")
Returns: { "upload_url": "https://strom.example.com/api/uploads/…", "method": "POST",
"max_bytes": 104857600, "expires_at": "2026-10-30T12:15:00Z" }

Then POST the raw bytes to upload_url with no Authorization header (curl --data-binary @report.csv "$upload_url"). The URL token is the credential. The URL works once and expires after 15 minutes (input_files.upload_url_ttl_secs). The token is used up when the request arrives, before the body is read: if the upload fails, ask for a new URL. A used, expired or unknown URL answers 404 Upload URL not found. When the upload arrives, the server checks again that the URL’s user may still run the task and that the task still has a file field. If not, it answers the same 404 Upload URL not found and the URL stays used. The answer holds the file_id. Pass it as the field value in execute_task.

create_upload_url needs auth.base_url in the server config. Without it the tool answers upload URLs need auth.base_url; send the file inline or use the REST upload. It needs the Run permission on the task (View only gives Insufficient permissions: execute requires Run access), a task with a file field (else task has no file input) and a usable file_name (else invalid file name).

When auth is configured on your Strøm server, MCP clients must authenticate using the standard HTTP Authorization header:

Authorization: Bearer <token>

Strøm supports two token types:

  • JWT tokens: Long-lived (30 days) user session tokens issued at login. Supported on both API keys and user logins.
  • API keys: Long-lived programmatic tokens with the strm_ prefix. Ideal for MCP integrations and CI/CD pipelines. Manage API keys via the Settings page or /api/auth/api-keys endpoint.

MCP clients without an Authorization header can still connect if auth is disabled on the server.

When auth is enabled, Strøm acts as both the OAuth 2.1 Resource Server for /mcp and the Authorization Server that issues tokens for it. A spec-conformant MCP client connects with no manual configuration — it discovers the auth flow on the first 401.

The flow:

  1. Client calls /mcp without a token.
  2. Strøm responds 401 Unauthorized with WWW-Authenticate: Bearer resource_metadata="https://.../.well-known/oauth-protected-resource".
  3. Client fetches that document — RFC 9728 Protected Resource Metadata — which advertises the authorization server URL.
  4. Client fetches /.well-known/oauth-authorization-server — RFC 8414 — to learn the endpoints, grant types, and PKCE methods. Strøm advertises:
    • response_types_supported: ["code"]
    • grant_types_supported: ["authorization_code", "refresh_token"]
    • code_challenge_methods_supported: ["S256"] (OAuth 2.1 forbids plain)
    • token_endpoint_auth_methods_supported: ["none", "client_secret_post"]
  5. Client self-registers via POST /oauth/register — RFC 7591 Dynamic Client Registration. The response includes a client_id (and client_secret if the client requested confidential auth).
  6. Client opens the user’s browser to /oauth/authorize?response_type=code&client_id=...&redirect_uri=...&code_challenge=...&code_challenge_method=S256&scope=mcp&resource=https://.../mcp&state=....
  7. Strøm validates the request, then redirects to /consent?... — the React consent screen. If the user isn’t logged in, the SPA bounces them through /login?next=/consent?... first.
  8. The user clicks Allow. The SPA POST /api/oauth/consent mints a single-use authorization code and returns the final redirect URL, which the browser then navigates to: redirect_uri?code=<code>&state=<state>.
  9. Client exchanges the code at POST /oauth/token with the matching code_verifier. Strøm verifies PKCE (BASE64URL(SHA256(code_verifier)) == stored code_challenge), then mints:
    • An access token — JWT, 1 hour TTL, aud bound to the canonical /mcp URL (RFC 8707 Resource Indicators).
    • A refresh token — opaque, DB-backed, 30 day TTL, rotated on each use (OAuth 2.1 §4.13).
  10. Client uses the access token as the Bearer value on /mcp. The MCP middleware enforces the aud claim — a token issued for a different resource is rejected, even if signed with the same secret.

POST /oauth/register is public — any client can register without an admin step. To limit abuse:

  • Each registration expires after 30 days. Long-running integrations re-register on the first request after expiry; the cost is one automatic POST.
  • Maximum 5 redirect_uris per client.
  • Only https://... redirects are allowed, except for http://localhost, http://127.0.0.1, and http://[::1] (loopback exceptions per OAuth 2.1 §4.1).
  • Only the authorization_code + refresh_token grants and the mcp scope are permitted.

Tokens issued by /oauth/token carry an aud claim equal to the canonical resource URL (e.g. https://stroem.example.com/mcp). The MCP middleware verifies this on every request. A token leaked from one resource server cannot be replayed against another, even if both trust the same issuer.

Legacy strm_* API keys and login-flow JWTs predate the OAuth flow and have no aud claim — the middleware accepts them unchanged. New integrations should prefer the OAuth flow.

Set auth.base_url in server-config.yaml so the discovery documents and aud claim resolve to the canonical public URL:

auth:
jwt_secret: "..."
refresh_secret: "..."
base_url: "https://stroem.example.com"

Without base_url, Strøm falls back to the request Host (respecting X-Forwarded-Host / X-Forwarded-Proto) — fine for local dev, brittle behind multiple proxy layers.

A spec-conformant MCP client only needs the /mcp URL. Auth handles itself:

{
"mcpServers": {
"stroem": {
"url": "https://stroem.example.com/mcp",
"transport": "streamable-http"
}
}
}

The first connection triggers the 401 → discovery → DCR → browser consent → token exchange flow described above. After the user clicks Allow once, Claude Desktop refreshes the access token silently for as long as the refresh token is valid (30 days).

To connect Claude Desktop to your Strøm server, add the MCP configuration to claude_desktop_config.json:

On macOS/Linux, edit ~/.claude/claude_desktop_config.json:

{
"mcpServers": {
"stroem": {
"url": "http://localhost:8080/mcp",
"transport": "streamable-http"
}
}
}

On Windows, edit %APPDATA%\Claude\claude_desktop_config.json:

{
"mcpServers": {
"stroem": {
"url": "http://localhost:8080/mcp",
"transport": "streamable-http"
}
}
}

If your Strøm server requires authentication, add the token:

{
"mcpServers": {
"stroem": {
"url": "http://localhost:8080/mcp",
"transport": "streamable-http",
"headers": {
"Authorization": "Bearer YOUR_API_KEY_OR_JWT_TOKEN"
}
}
}
}

Restart Claude Desktop after updating the config. The Strøm tools will appear in Claude’s tool palette.

Here’s a typical interaction sequence between an AI agent and Strøm via MCP:

Call: list_workspaces
Returns:
[
{
"name": "production",
"display_name": "Production",
"description": "Customer-facing deploys and jobs",
"task_count": 12
},
{
"name": "staging",
"task_count": 8
}
]
Call: list_tasks(workspace="production")
Returns:
[
{ "name": "deploy-api", "description": "Deploy API server" },
{ "name": "run-tests", "description": "Run test suite" },
{ "name": "backup-db", "description": "Backup production database" }
]
Call: get_task(workspace="production", task_name="deploy-api")
Returns:
{
"name": "deploy-api",
"description": "Deploy API server",
"input": {
"version": { "type": "string", "required": true },
"environment": { "type": "string", "default": "staging" }
},
"flow": [
{ "name": "pull-image", "action": "docker-pull", ... },
{ "name": "start-container", "action": "docker-run", "depends_on": ["pull-image"] }
]
}
Call: execute_task(
workspace="production",
task_name="deploy-api",
input={ "version": "v1.2.3" }
)
Returns:
{
"job_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}

input must be a JSON object mapping the task’s field names to values. If your MCP client serializes it as a JSON-encoded string (some clients do), the server parses it back into an object automatically; anything that isn’t a JSON object is rejected with an error rather than silently run with default values.

Call: get_job_status(job_id="a1b2c3d4-e5f6-7890-abcd-ef1234567890")
Returns:
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"status": "running",
"started_at": "2025-03-12T14:30:00Z",
"steps": [
{ "name": "pull-image", "status": "completed", "duration_secs": 45 },
{ "name": "start-container", "status": "running", "started_at": "2025-03-12T14:30:50Z" }
]
}
Call: get_job_logs(job_id="a1b2c3d4-e5f6-7890-abcd-ef1234567890")
Returns:
[pull-image] Pulling image us-docker.io/stroem/api:v1.2.3...
[pull-image] Pull complete (45.2s)
[start-container] Starting container stroem-api-prod...
[start-container] Container started with ID abc123def456
[start-container] Health check: passed

This shows the tail of the log (last 256 KiB by default); pass tail_bytes for more history, up to log_storage.read.tail_max_bytes.

get_job_status masks secret values (••••••) in what it returns — chiefly the steps’ error_message — the same way as GET /api/jobs/{id}, including the secrets of any pinned commit the job is connected to. When those secrets cannot be loaded at the moment, the tool returns an error, redaction set unavailable, retry, instead of the job; call it again later. When a pinned commit can never be loaded again, the job is returned with every content string masked. A job that runs on a git ref carries its ref, in get_job_status and in list_jobs.

get_job_logs returns log lines as the steps printed them; log lines are not masked.

Agents can handle multi-step deployments with approval gates, automated rollbacks on health check failures, and notification of relevant teams.

Agents can query job logs, identify failing steps, and suggest remediation steps based on error messages and logs.

External CI/CD systems can use MCP to trigger Strøm tasks, poll job status, and integrate results into larger pipelines.

Agents can explore available tasks, their input requirements, and past execution patterns to answer operational questions.

When ACL is configured on your Strøm server, MCP tools enforce the same permission rules as the REST API:

  • List tools (list_workspaces, list_tasks, list_jobs): Filter results to only show resources the user has access to. Tasks with Deny permission are hidden. list_jobs filters before applying limit, so a page holds up to limit jobs you may see.
  • Read tools (get_task, get_job_status, get_job_logs, list_artifacts, get_artifact): Require View or Run permission. Deny returns “not found”. A job that runs on a git ref is checked against the folder its own commit declares for its task.
  • Mutation tools (execute_task, cancel_job): Require Run permission. View-only users receive an error.

The list_tasks response includes a can_execute field indicating whether the user can execute each task (true for Run, false for View).

Admin users bypass ACL over MCP, exactly as they do in the UI and REST API. Access mirrors the user’s actual privileges: an admin sees every workspace, task, and job, while a non-admin is limited to what the ACL grants their email or groups. This holds for both authentication methods — API keys and the OAuth flow. If you authenticate as an admin and still see empty lists, confirm you connected as an admin user (see Troubleshooting below).

Jobs created via MCP are tagged with source_type = "mcp" and the authenticated user’s email as source_id for audit trail.

  • Use API keys for integrations: Create dedicated API keys for MCP connections rather than sharing user credentials.
  • Set token expiry: When creating API keys for long-running integrations, consider setting an expiry date for security.
  • Monitor MCP requests: MCP operations are logged with the authenticated user’s email as source_id for audit tracking.
  • Validate agent outputs: When agents execute tasks, ensure their input validation matches your requirements (schemas are available via get_task).
  • Use descriptive error messages: Include clear error_message fields in your task actions so agents can provide helpful troubleshooting guidance.

Ensure your Strøm server is running and accessible at the configured URL. If behind a proxy or load balancer, verify that the /mcp endpoint is not blocked or rewritten.

Verify your API key or JWT token is valid and included in the Authorization header. API keys must start with strm_ prefix. Check the server logs for auth-related errors.

Ensure MCP is enabled in server-config.yaml and your server has been restarted. Tools are only available after the server starts with mcp.enabled: true.

Verify the workspace and task names are correct (case-sensitive). Use list_workspaces and list_tasks to confirm availability.

Empty Lists / “Not Found” for Everything

Section titled “Empty Lists / “Not Found” for Everything”

If every list tool returns empty and reads/executes report “not found”, the authenticated identity has no ACL grants. MCP access mirrors the user’s actual privileges, so check: (1) you are connected as an admin user (admins bypass ACL), or (2) your email or a group you belong to is granted access in the server’s acl configuration. Note that the DB is_admin flag is what matters — it is honored for both API keys and OAuth-authenticated sessions.