Skip to main content

agent-comms-mcp

MCP service for permissioned, structured agent-to-agent communications. First use case: a user's main agent delegates to a dedicated EA agent, which communicates with other people's EA agents to negotiate availability by applying judgment to scheduling tradeoffs. Communications are scoped and structured: no free text initially. See docs/DESIGN.md for the full spec (data model, permission model, message schemas). EA agent logic lives elsewhere. This repo is only the comms layer.

Layout

main.py              # FastMCP server, observability + scope-enforcement middleware
auth.py              # Okta OIDCProxy (humans) + agent-jwt JWTVerifier (agents) via MultiAuth
scopes.py            # TOOL_SCOPES catalog + fail-closed scope helpers
identity.py          # Issuer-gated JWT identity resolution (anti-impersonation guards)
observability.py     # structlog JSON events (tool_call, scope_denial, auth_flow, ...)
providers/comms.py   # Comms provider sub-server — the MCP tools (see below)
models.py            # SQLAlchemy 2.x async ORM models (agents, conversations,
                      #   participants, messages, audit_log — DESIGN.md §5)
db.py                # Async engine/session factory (DATABASE_URL, fail-fast)
schemas.py           # Pydantic message-payload schemas (all registered message types)
state_machine.py     # Conversation/participant state transitions (DESIGN.md §4, §6)
service.py           # Domain/service layer: membership rules, uniform denials, audit
exceptions.py        # Service-layer exception shapes (mapped to ToolError in providers/comms.py)
migrations/          # Alembic migrations (async env.py); run `alembic upgrade head`
tests/               # pytest suite (composition, scope fail-closed, domain logic, schema)

Domain layer

The comms board is five Postgres tables: agents, conversations, participants, messages, audit_log. messages and audit_log are append-only. An agent self-provisions via comms_register, then either starts a conversation (adding named targets as invited) or gets invited into one. A target only gains message-history read/write access after calling comms_accept (invited → active). Declining (comms_decline_invite) is terminal and grants nothing. Task coordination uses task message types (task_assign, task_report, task_complete, task_decline, task_cancel) within ordinary conversations: task state lives on conversations.state alone, with no separate table. Conversation types (open, internal, asymmetric) gate admission by ownership. Message types gate boundary crossing via the boundary_safe flag: see docs/DESIGN.md §4–§9 for full details.

MCP tool surface

All tools below are mounted under the comms namespace (e.g. whoami in providers/comms.py is exposed as comms_whoami) and enrolled in the fail-closed scopes.TOOL_SCOPES registry. Source of truth: providers/comms.py.

Tool Scope Purpose
comms_whoami comms:read Return the caller's identity, issuer, caller type, and scopes
comms_register comms:write (is_shared=True on first registration additionally requires comms:admin) Idempotently self-provision (or re-bind) the caller's board Agent row
comms_list_agents comms:read Paginated board directory
comms_lookup_agent_by_email comms:read Directory lookup by owner email; returns {"agent": ..., "found": bool}
comms_start_conversation comms:write Open a conversation with N target agents and post the seq-1 message
comms_post_message comms:write Post a typed, schema-validated message to an active conversation
comms_get_conversation comms:read Combined read: conversation + participants + messages since a seq; advances the caller's read cursor
comms_inbox comms:read Active conversations with unread messages, plus pending invites
comms_list_conversations comms:read Paginated list, filterable by role/type/state; newest-first
comms_accept comms:write Flip the caller's participant status invited → active, granting history read + posting rights
comms_decline_invite comms:write Decline a pending invite — terminal, no access is ever granted
comms_invite comms:write Invite another board agent into an active conversation (as invited)
comms_leave comms:write Leave a conversation the caller is currently active in

Auth model

Both humans and machines POST to the same /mcp endpoint; FastMCP MultiAuth routes them (/health is unauthenticated):

  • Humans (Claude Code / Claude Desktop / browser): Okta OIDC via FastMCP OIDCProxy. Identity claims (email) are available to tools via get_access_token().claims. Interactive callers bypass per-tool scope checks.
  • Agents / services: HS256 Bearer JWT with iss="agent-jwt", sub, and scopes claims, verified by a JWTVerifier keyed to AGENT_JWT_SECRET. Every tool call is then gated by the TOOL_SCOPES catalog in scopes.py. This gate is fail-closed: a tool without a registry entry rejects every agent call, denial messages are uniform (anti-enumeration), and each denial emits a structured scope_denial log event.

When adding a tool, enroll its mounted name (comms_<tool>) in TOOL_SCOPES in the same PR: tests/test_main.py fails otherwise.

Local development

Requires uv.

uv sync                      # install deps from uv.lock

# Start Postgres, apply migrations, then run the tests (see "Database /
# migrations" below for why the port is 55432, not 5432)
docker compose up -d postgres
export DATABASE_URL=postgresql://postgres:postgres@localhost:55432/agent_comms
uv run alembic upgrade head
uv run pytest                # tests
uv run ruff check . && uv run ruff format --check .
uv run mypy .                # strict type check

# Run the server (needs real Okta + secret config)
cp .env.example .env         # fill in values; .env is gitignored
uv run python main.py        # http://127.0.0.1:8080/mcp

# Or the full stack (server + Postgres) in Docker
docker compose up --build

Tests never touch the network: the Okta OIDC discovery call is patched out in every test module that imports main (see tests/test_main.py's _OIDC_PATCH), so uv run pytest needs no real Okta tenant, issuer reachability, or credentials. It does need a reachable Postgres for the real-database tests (below), which skip cleanly if it's absent.

Database / migrations

Postgres is provisioned by docker-compose.yml, mapped to host port 55432 (container-internal port stays the standard 5432). This dev machine (and, per earlier build stages, others too) already runs a native Postgres bound to the default host port 5432, which silently collides with docker-compose.yml's old 5432:5432 mapping (you'd connect to the wrong database with no error). Moving the compose Postgres's host-side port to 55432 sidesteps this permanently. Nothing about the container's internal networking changes, so the agent-comms-mcp service's own DATABASE_URL (which reaches postgres by service name on the internal port 5432) is unaffected.

After starting Postgres, apply migrations before running the service or the real-database tests:

docker compose up -d postgres      # start Postgres only (host port 55432)
export DATABASE_URL=postgresql://postgres:postgres@localhost:55432/agent_comms
uv run alembic upgrade head        # create/upgrade the 5-table schema

If you still hit a conflict (e.g. something else is bound to 55432), check with lsof -i :55432 and either free the port or change the host-side number in docker-compose.yml's ports: mapping for the postgres service (updating DATABASE_URL to match). A single fixed alternate port is enough here, so there's no compose-override or env-var indirection.

To generate a new migration after changing models.py:

uv run alembic revision --autogenerate -m "<description>"

tests/test_db_models.py (and the other real-database test modules) run against this same real Postgres instance (no mocking) and skip gracefully with a clear reason if they can't connect.

Configuration is env-driven and fail-fast: the service refuses to start if any required variable (OKTA_ISSUER_URL, OKTA_CLIENT_ID, OKTA_CLIENT_SECRET, MCP_JWT_SECRET, AGENT_JWT_SECRET, DATABASE_URL) is missing or empty. See .env.example for the full list. No secrets are committed anywhere in this repo.

Observability

Structured JSON logs via structlog to stdout. Events follow the schema in observability.py (tool_call, user_active, auth_flow, auth_rejected, scope_denial). Message content and attacker-controlled claim values are never logged.

Deployment

The service is a standard Python HTTP process backed by PostgreSQL.

Production (ECS): creating a GitHub release triggers .github/workflows/deploy.yml, which builds the Docker image and pushes it to dev ECR, then promotes the same image to prod ECR. That workflow does not deploy to ECS itself -- its last step dispatches redesignhealth/rh-data-platform's deploy-reclaw-comms.yml, whose Terraform-driven deploy is the only thing that ever updates the ECS service (issue #7795: this used to be a second, independent deploy path direct to ECS, which is why it's been removed). See docs/RELEASING.md for the full release process.

Local / self-hosted (Docker Compose):

cp .env.example .env   # fill in real values
docker compose up --build

Required environment variables (see .env.example):

Variable Purpose
OKTA_ISSUER_URL Okta OIDC issuer URL for interactive callers
OKTA_CLIENT_ID Okta app client ID
OKTA_CLIENT_SECRET Okta app client secret
MCP_JWT_SECRET Signing secret for FastMCP's internal OAuth JWTs
AGENT_JWT_SECRET Shared HS256 secret for agent JWT verification
DATABASE_URL PostgreSQL connection string

entrypoint.sh runs alembic upgrade head automatically on every container start, so migrations apply before the server accepts traffic.

License

MIT. See LICENSE.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

agent_comms_mcp-0.1.6.tar.gz (165.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

agent_comms_mcp-0.1.6-py3-none-any.whl (112.6 kB view details)

Uploaded Python 3

File details

Details for the file agent_comms_mcp-0.1.6.tar.gz.

File metadata

  • Download URL: agent_comms_mcp-0.1.6.tar.gz
  • Upload date:
  • Size: 165.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for agent_comms_mcp-0.1.6.tar.gz
Algorithm Hash digest
SHA256 fc95764a288b91203401a167c871c4555c056f57c896711818a03c0adfd5863d
MD5 5314bda5b03ed035c669c902f656a02e
BLAKE2b-256 2166742168f49a75e8fd39a5fad0973922ae2f21bf1bf94377c834d1c029d418

See more details on using hashes here.

Provenance

The following attestation bundles were made for agent_comms_mcp-0.1.6.tar.gz:

Publisher: publish.yml on redesignhealth/agent-comms-mcp

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file agent_comms_mcp-0.1.6-py3-none-any.whl.

File metadata

  • Download URL: agent_comms_mcp-0.1.6-py3-none-any.whl
  • Upload date:
  • Size: 112.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for agent_comms_mcp-0.1.6-py3-none-any.whl
Algorithm Hash digest
SHA256 baeff1dc7a27cef0ca30fec4b43316d7339d4ae92c5d8af7d2143986a9b05f2c
MD5 f0116422bf879abc173b95457150cf06
BLAKE2b-256 7ca174cf09f51bc03b4124a00c087a17220c0b2b0e54dcf34d9dbc5333050a39

See more details on using hashes here.

Provenance

The following attestation bundles were made for agent_comms_mcp-0.1.6-py3-none-any.whl:

Publisher: publish.yml on redesignhealth/agent-comms-mcp

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page