Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
94a6f96
Support streaming with mcp
kam-validmind Jul 22, 2026
fbe3f6e
more testing
kam-validmind Jul 23, 2026
0bdf40d
Merge branch 'main' into sse_mcp
kam-validmind Jul 23, 2026
05e2b96
fix: keep active standalone streams alive
kam-validmind Jul 23, 2026
eb95cea
docs: clarify streaming transport contracts
kam-validmind Jul 23, 2026
75567da
refactor: separate streaming responsibilities
kam-validmind Jul 23, 2026
0a3ae30
fix: harden upstream stream recovery
kam-validmind Jul 23, 2026
89bfdf3
fix: make stream auditing bounded and truthful
kam-validmind Jul 23, 2026
4e11271
fix: address stream relay review findings
kam-validmind Jul 23, 2026
eb8f738
finalize buffered calls after disconnect; harden relay lifecycle
kam-validmind Jul 23, 2026
522d6a8
Merge branch 'main' into sse_mcp
kam-validmind Jul 24, 2026
f62ecc0
fix: close standalone-stream connect race before sending tools/call
kam-validmind Jul 24, 2026
38ce518
docs: document SSE relay control flow in pseudocode
kam-validmind Jul 24, 2026
4864436
test: add streaming coverage to the harness integration suite
kam-validmind Jul 24, 2026
f8d3cc6
docs: clarify upstream/downstream terminology in system context
kam-validmind Jul 24, 2026
af2efe8
improved docs with more scenarios
kam-validmind Jul 27, 2026
8f22e35
test: pin the multi-replica approval race and diagram it inline
kam-validmind Jul 27, 2026
c6abf0c
fix: bound terminal stream cleanup
kam-validmind Jul 27, 2026
28d30ae
test: extract pollUntil and newTestAgentServer to cut duplication
kam-validmind Jul 27, 2026
6909bd9
add more comments on the sequences of things
kam-validmind Jul 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,54 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Live SSE relay for `tools/call`: when an upstream MCP server answers a tool
call with a Server-Sent Events stream (Streamable HTTP transport, MCP spec
2025-11-25), Atryum relays intermediate messages (progress, logging, other
notifications) to the connected agent as they arrive, instead of buffering
the whole response and returning only the terminal result. Activates only
when the agent's request sends `Accept: text/event-stream` and the
upstream responds with one; non-streaming tool calls are unaffected.
If an upstream closes a resumable SSE response before the terminal
JSON-RPC message, Atryum reconnects with `Last-Event-ID` and continues
from the last event acknowledged to the upstream.
Streamed events and terminal delivery are audited into `invocation_events`
(`invocation.stream_event`, `invocation.stream_completed`, and
`invocation.stream_delivery`). New
`[defaults]` config knobs: `stream_relay_enabled` (kill-switch, default
on), `stream_header_timeout_seconds`, `stream_idle_timeout_seconds`,
`stream_max_duration_seconds`, `stream_max_message_bytes`,
`stream_audit_max_events`,
`stream_audit_max_event_bytes`. See `docs/architecture.md` for the full
design.
- The relay also listens on the Streamable HTTP standalone SSE stream (a GET
to the upstream endpoint, independent of any specific `tools/call`), since
some upstream SDKs (e.g. the reference MCP Python SDK's
`Context.report_progress`) send progress notifications there rather than on
the `tools/call` response itself. Atryum rewrites each call's
`_meta.progressToken` to a value unique to that call before forwarding it
upstream, so two unrelated concurrent callers who happen to choose the same
token can never have their progress notifications cross-delivered.
Standalone connections now follow session renewal and retry transient
failures with bounded backoff. Stream audit writes use a fixed shared worker
pool; standalone-buffer drops and downstream terminal-delivery outcomes are
recorded explicitly.
- Relay behavior guarantees: server-initiated upstream requests (sampling,
elicitation, roots) are audited but never forwarded to the agent, whichever
connection carried them; SSE keepalive/comment lines count as upstream
activity for the idle timeout, so a busy-but-quiet tool heartbeating through
a long run is not cut off; and stream failure audit reasons distinguish a
proven downstream abort (`stream_aborted_downstream`, only when a write to
the agent failed) from a bare request-context cancellation
(`stream_canceled`) and upstream timeouts (`stream_timeout`).

### Fixed

- Buffered (non-streaming) tool calls whose downstream client disconnects
mid-call no longer leave the invocation row stuck `executing`: finalization
writes now run on their own bounded context, matching the streaming path.

## [0.2.0] - 2026-07-14

### Added
Expand Down
18 changes: 18 additions & 0 deletions Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,24 @@ judge-eval-check:
go test -tags judgeeval ./internal/invocation \
-run 'TestJudge(GarbageOutput|MarkdownFenced|Request|UnrecognizedVerdict)|TestConstantVerdictBaselines' -v

# Real end-to-end test of the tools/call SSE relay against the official MCP
# reference "everything" server (@modelcontextprotocol/server-everything),
# spawned live via npx over its Streamable HTTP transport. Requires
# Node/npm; network access on first run to fetch the package. Skips itself
# if npx isn't on PATH. See internal/api/mcp_everything_test.go.
mcp-everything-test:
go test -tags mcpeverything ./internal/api -run TestMCPToolsCallAgainstRealEverythingServer -v

# Real end-to-end test of the tools/call SSE relay's standalone-stream path,
# against a real MCP Python SDK (FastMCP) server spawned live via uv.
# FastMCP's Context.report_progress sends progress on the standalone SSE
# stream, never the tools/call response itself — the complement to
# mcp-everything-test above. Requires uv (https://docs.astral.sh/uv/);
# network access on first run to resolve the mcp package. Skips itself if uv
# isn't on PATH. See internal/api/mcp_standalone_stream_test.go.
mcp-standalone-stream-test:
go test -tags mcpstandalone ./internal/api -run TestMCPToolsCallAgainstRealStandaloneStreamServer -v

# List registered harnesses, auth protocols, and MCP targets
integration-list:
integrations/scripts/agent_harness_integration_tests.sh list
Expand Down
17 changes: 16 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ The embedded invocation view subscribes to the admin SSE stream, updates list/de

## Storage

SQLite by default, PostgreSQL optional via `server.database_url`. Both are first-class — migrations live in `internal/store/migrations/` and apply at startup. Core tables:
SQLite by default, PostgreSQL optional via `server.database_url`. Both are first-class. Built-in migration definitions live in `pkg/migrations/`; `internal/store` applies and tracks them at startup. Embedding programs can add separately tracked, namespaced migrations through `pkg/atryum.WithMigrations`. Core tables:

- `mcp_servers` — upstream connection settings and generic auth/connection status, including `connection_status`, `auth_status`, `reauth_needed`, `auth_type`, `last_checked_at`, `last_check_ok`, `last_error_summary`, and `action_required`.
- `oauth_credentials` and related OAuth client registration tables — tokens and client registrations held by Atryum on behalf of agents.
Expand Down Expand Up @@ -218,6 +218,21 @@ After first-run bootstrap, edit MCP servers through the UI/API; TOML `[[upstream

When `backend.base_url` is empty, the ValidMind backend connection check is skipped for local standalone runs. When it is set, startup fails if credentials are missing or `GET /api/atryum/unstable/connection` is rejected. Environment variables override TOML: `VM_BASE_URL`, `VM_MACHINE_KEY`, `VM_MACHINE_SECRET`, and `VM_CONNECTION_TIMEOUT_SECONDS`.

## Embedding

The stock executable in `cmd/atryum` is a thin wrapper around the public
`pkg/atryum` package. Another Go program can call `atryum.Main` to run the same CLI and
server with these options:

- `WithRoutes` adds HTTP routes. Extension routes are outside Atryum's built-in
authentication middleware and must authenticate themselves.
- `WithMigrations` adds namespaced migrations after the built-in sequence.
- `WithDatabase` runs a database hook after all migrations and before the server starts.
- `WithThirdPartyNotices` supplies the notices printed by the `licenses` command.

Extensions run inside the Atryum process and share its HTTP server and database. See
[`pkg/atryum`](pkg/atryum) and [`pkg/migrations`](pkg/migrations) for the public types.

## Running

Single-binary Go service.
Expand Down
26 changes: 26 additions & 0 deletions atryum.example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,32 @@ connection_timeout_seconds = 5
[defaults]
request_timeout_seconds = 30

# Live relay for tools/call. When the agent accepts SSE, Atryum can forward
# intermediate HTTP SSE or stdio JSON-RPC messages as they arrive, followed
# by one terminal SSE response. A plain upstream JSON response remains plain
# downstream. stream_relay_enabled disables the relay without a rollback.
stream_relay_enabled = true
# Bounds HTTP session initialization and tools/call response headers, or the
# stdio initialize handshake. 0 falls back to request_timeout_seconds above.
stream_header_timeout_seconds = 0
# Bounds the gap in upstream activity while reading the response. Resets on
# every message, and on SSE keepalive/comment lines, so a quiet-but-alive
# upstream is not cut off. Does not bound the call's total duration.
stream_idle_timeout_seconds = 60
# Bounds the complete upstream response-reading phase. 0 = unlimited.
stream_max_duration_seconds = 600
# Rejects one upstream SSE event, stdio JSON-RPC line, or plain JSON response
# body larger than this many bytes. This bounds memory used by one message.
stream_max_message_bytes = 4194304
# Caps how many invocation.stream_event audit rows are persisted per call.
# Beyond the cap, events are still relayed live to the agent but only
# counted, not stored individually. 0 disables this count cap. The bounded
# shared audit queue reports storage backpressure drops in stream_completed.
stream_audit_max_events = 100
# Truncates each persisted stream_event row's data field beyond this many
# bytes. 0 = no truncation.
stream_audit_max_event_bytes = 4096

# Optional Claude Managed Agents events bridge. Declare one [[managed_agents]]
# table per Anthropic account/workspace you want to watch. When an entry's
# api_key is set, Atryum connects outbound to Anthropic's Managed Agents
Expand Down
Loading
Loading