agent-comms-mcp 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. agent_comms_mcp-0.1.0/LICENSE +21 -0
  2. agent_comms_mcp-0.1.0/PKG-INFO +221 -0
  3. agent_comms_mcp-0.1.0/README.md +198 -0
  4. agent_comms_mcp-0.1.0/agent_comms_mcp.egg-info/PKG-INFO +221 -0
  5. agent_comms_mcp-0.1.0/agent_comms_mcp.egg-info/SOURCES.txt +41 -0
  6. agent_comms_mcp-0.1.0/agent_comms_mcp.egg-info/dependency_links.txt +1 -0
  7. agent_comms_mcp-0.1.0/agent_comms_mcp.egg-info/entry_points.txt +2 -0
  8. agent_comms_mcp-0.1.0/agent_comms_mcp.egg-info/requires.txt +9 -0
  9. agent_comms_mcp-0.1.0/agent_comms_mcp.egg-info/top_level.txt +13 -0
  10. agent_comms_mcp-0.1.0/auth.py +343 -0
  11. agent_comms_mcp-0.1.0/db.py +70 -0
  12. agent_comms_mcp-0.1.0/exceptions.py +107 -0
  13. agent_comms_mcp-0.1.0/identity.py +102 -0
  14. agent_comms_mcp-0.1.0/main.py +305 -0
  15. agent_comms_mcp-0.1.0/migrations/env.py +100 -0
  16. agent_comms_mcp-0.1.0/migrations/versions/15ef34885e30_add_conversations_owner_snapshot.py +65 -0
  17. agent_comms_mcp-0.1.0/migrations/versions/18f2d7735523_rate_limit_indexes_and_display_name_.py +124 -0
  18. agent_comms_mcp-0.1.0/migrations/versions/6d2a8e63e469_add_tasks_table.py +127 -0
  19. agent_comms_mcp-0.1.0/migrations/versions/da3e1646c44d_drop_tasks_table.py +194 -0
  20. agent_comms_mcp-0.1.0/migrations/versions/e1db7c2e6b70_backfill_accepted_types_for_existing_.py +92 -0
  21. agent_comms_mcp-0.1.0/migrations/versions/ef8394b37c8d_initial_schema.py +190 -0
  22. agent_comms_mcp-0.1.0/models.py +232 -0
  23. agent_comms_mcp-0.1.0/observability.py +245 -0
  24. agent_comms_mcp-0.1.0/providers/__init__.py +6 -0
  25. agent_comms_mcp-0.1.0/providers/comms.py +755 -0
  26. agent_comms_mcp-0.1.0/pyproject.toml +85 -0
  27. agent_comms_mcp-0.1.0/schemas.py +544 -0
  28. agent_comms_mcp-0.1.0/scopes.py +170 -0
  29. agent_comms_mcp-0.1.0/service.py +2264 -0
  30. agent_comms_mcp-0.1.0/setup.cfg +4 -0
  31. agent_comms_mcp-0.1.0/state_machine.py +143 -0
  32. agent_comms_mcp-0.1.0/tests/test_auth.py +426 -0
  33. agent_comms_mcp-0.1.0/tests/test_comms_tools.py +1582 -0
  34. agent_comms_mcp-0.1.0/tests/test_db_models.py +297 -0
  35. agent_comms_mcp-0.1.0/tests/test_identity.py +145 -0
  36. agent_comms_mcp-0.1.0/tests/test_main.py +522 -0
  37. agent_comms_mcp-0.1.0/tests/test_migrations_offline.py +80 -0
  38. agent_comms_mcp-0.1.0/tests/test_observability.py +205 -0
  39. agent_comms_mcp-0.1.0/tests/test_schemas.py +537 -0
  40. agent_comms_mcp-0.1.0/tests/test_scopes.py +187 -0
  41. agent_comms_mcp-0.1.0/tests/test_service.py +3030 -0
  42. agent_comms_mcp-0.1.0/tests/test_state_machine.py +185 -0
  43. agent_comms_mcp-0.1.0/tests/test_whoami.py +71 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Redesign Health
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,221 @@
1
+ Metadata-Version: 2.4
2
+ Name: agent-comms-mcp
3
+ Version: 0.1.0
4
+ Summary: MCP service for permissioned, structured agent-to-agent communications
5
+ Author-email: Redesign Health <tech@redesignhealth.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/redesignhealth/agent-comms-mcp
8
+ Project-URL: Repository, https://github.com/redesignhealth/agent-comms-mcp
9
+ Project-URL: Issues, https://github.com/redesignhealth/agent-comms-mcp/issues
10
+ Requires-Python: >=3.12
11
+ Description-Content-Type: text/markdown
12
+ License-File: LICENSE
13
+ Requires-Dist: alembic==1.18.5
14
+ Requires-Dist: asyncpg==0.31.0
15
+ Requires-Dist: fastmcp==3.4.2
16
+ Requires-Dist: greenlet==3.5.4
17
+ Requires-Dist: httpx==0.28.1
18
+ Requires-Dist: py-key-value-aio[filetree]==0.4.5
19
+ Requires-Dist: pydantic==2.12.5
20
+ Requires-Dist: sqlalchemy[asyncio]==2.0.51
21
+ Requires-Dist: structlog==25.5.0
22
+ Dynamic: license-file
23
+
24
+ # agent-comms-mcp
25
+
26
+ MCP service for **permissioned, structured agent-to-agent communications**.
27
+ First use case: a user's main agent delegates to a dedicated EA agent, which
28
+ communicates with other people's EA agents to negotiate availability (including
29
+ judgment, not just calendar overlap). Communications are scoped and structured
30
+ — no free text initially. See [`docs/DESIGN.md`](docs/DESIGN.md) for the full
31
+ spec (data model, permission model, message schemas). EA agent logic lives
32
+ elsewhere — this repo is only the comms layer.
33
+
34
+ ## Layout
35
+
36
+ ```
37
+ main.py # FastMCP server, observability + scope-enforcement middleware
38
+ auth.py # Okta OIDCProxy (humans) + agent-jwt JWTVerifier (agents) via MultiAuth
39
+ scopes.py # TOOL_SCOPES catalog + fail-closed scope helpers
40
+ identity.py # Issuer-gated JWT identity resolution (anti-impersonation guards)
41
+ observability.py # structlog JSON events (tool_call, scope_denial, auth_flow, ...)
42
+ providers/comms.py # Comms provider sub-server — the MCP tools (see below)
43
+ models.py # SQLAlchemy 2.x async ORM models (agents, conversations,
44
+ # participants, messages, audit_log — DESIGN.md §5)
45
+ db.py # Async engine/session factory (DATABASE_URL, fail-fast)
46
+ schemas.py # Pydantic message-payload schemas (all registered message types)
47
+ state_machine.py # Conversation/participant state transitions (DESIGN.md §4, §6)
48
+ service.py # Domain/service layer: membership rules, uniform denials, audit
49
+ exceptions.py # Service-layer exception shapes (mapped to ToolError in providers/comms.py)
50
+ migrations/ # Alembic migrations (async env.py); run `alembic upgrade head`
51
+ tests/ # pytest suite (composition, scope fail-closed, domain logic, schema)
52
+ ```
53
+
54
+ ## Domain layer
55
+
56
+ The comms board is five Postgres tables — `agents`, `conversations`,
57
+ `participants`, `messages`, `audit_log` — with `messages` and
58
+ `audit_log` append-only. An agent self-provisions via `comms_register`,
59
+ then either starts a conversation (adding named targets as `invited`) or
60
+ gets invited into one. A target only gains message-history read/write
61
+ access after calling `comms_accept` (`invited → active`); declining
62
+ (`comms_decline_invite`) is terminal and grants nothing. Task coordination
63
+ uses task message types (`task_assign`, `task_report`, `task_complete`,
64
+ `task_decline`, `task_cancel`) within ordinary conversations — task state
65
+ lives on `conversations.state`, not a separate table. Conversation types
66
+ (`open`, `internal`, `asymmetric`) gate admission by ownership; message
67
+ types gate boundary crossing via the `boundary_safe` flag — see
68
+ [`docs/DESIGN.md`](docs/DESIGN.md) §4–§9 for full details.
69
+
70
+ ## MCP tool surface
71
+
72
+ All tools below are mounted under the `comms` namespace (e.g. `whoami` in
73
+ `providers/comms.py` is exposed as `comms_whoami`) and enrolled in the
74
+ fail-closed `scopes.TOOL_SCOPES` registry. Source of truth:
75
+ `providers/comms.py`.
76
+
77
+ | Tool | Scope | Purpose |
78
+ |---|---|---|
79
+ | `comms_whoami` | `comms:read` | Return the caller's identity, issuer, caller type, and scopes |
80
+ | `comms_register` | `comms:write` | Idempotently self-provision (or re-bind) the caller's board `Agent` row |
81
+ | `comms_list_agents` | `comms:read` | Paginated board directory |
82
+ | `comms_start_conversation` | `comms:write` | Open a conversation with N target agents and post the seq-1 message |
83
+ | `comms_post_message` | `comms:write` | Post a typed, schema-validated message to an active conversation |
84
+ | `comms_get_conversation` | `comms:read` | Combined read: conversation + participants + messages since a seq; advances the caller's read cursor |
85
+ | `comms_inbox` | `comms:read` | Active conversations with unread messages, plus pending invites |
86
+ | `comms_list_conversations` | `comms:read` | Paginated list, filterable by role/type/state; newest-first |
87
+ | `comms_accept` | `comms:write` | Flip the caller's participant status `invited → active`, granting history read + posting rights |
88
+ | `comms_decline_invite` | `comms:write` | Decline a pending invite — terminal, no access is ever granted |
89
+ | `comms_invite` | `comms:write` | Invite another board agent into an active conversation (as `invited`) |
90
+ | `comms_leave` | `comms:write` | Leave a conversation the caller is currently `active` in |
91
+
92
+ ## Auth model
93
+
94
+ Both humans and machines POST to the same `/mcp` endpoint; FastMCP
95
+ `MultiAuth` routes them (`/health` is unauthenticated):
96
+
97
+ - **Humans** (Claude Code / Claude Desktop / browser): Okta OIDC via FastMCP
98
+ `OIDCProxy`. Identity claims (email) are available to tools via
99
+ `get_access_token().claims`. Interactive callers bypass per-tool scope
100
+ checks.
101
+ - **Agents / services**: HS256 Bearer JWT with `iss="agent-jwt"`, `sub`, and
102
+ `scopes` claims, verified by a `JWTVerifier` keyed to `AGENT_JWT_SECRET`.
103
+ Every tool call is then gated by the `TOOL_SCOPES` catalog in `scopes.py`
104
+ — **fail-closed**: a tool without a registry entry rejects every agent
105
+ call, denial messages are uniform (anti-enumeration), and each denial emits
106
+ a structured `scope_denial` log event.
107
+
108
+ When adding a tool, enroll its mounted name (`comms_<tool>`) in
109
+ `TOOL_SCOPES` in the same PR — `tests/test_main.py` fails otherwise.
110
+
111
+ ## Local development
112
+
113
+ Requires [uv](https://docs.astral.sh/uv/).
114
+
115
+ ```bash
116
+ uv sync # install deps from uv.lock
117
+
118
+ # Start Postgres, apply migrations, then run the tests (see "Database /
119
+ # migrations" below for why the port is 55432, not 5432)
120
+ docker compose up -d postgres
121
+ export DATABASE_URL=postgresql://postgres:postgres@localhost:55432/agent_comms
122
+ uv run alembic upgrade head
123
+ uv run pytest # tests
124
+ uv run ruff check . && uv run ruff format --check .
125
+ uv run mypy . # strict type check
126
+
127
+ # Run the server (needs real Okta + secret config)
128
+ cp .env.example .env # fill in values; .env is gitignored
129
+ uv run python main.py # http://127.0.0.1:8080/mcp
130
+
131
+ # Or the full stack (server + Postgres) in Docker
132
+ docker compose up --build
133
+ ```
134
+
135
+ Tests never touch the network: the Okta OIDC discovery call is patched out
136
+ in every test module that imports `main` (see `tests/test_main.py`'s
137
+ `_OIDC_PATCH`), so `uv run pytest` needs no real Okta tenant, issuer
138
+ reachability, or credentials — only a reachable Postgres for the
139
+ real-database tests (below), which skip cleanly if it's absent.
140
+
141
+ ### Database / migrations
142
+
143
+ Postgres is provisioned by `docker-compose.yml`, mapped to **host port
144
+ 55432** (container-internal port stays the standard 5432). This dev
145
+ machine — and, per earlier build stages, others too — already runs a
146
+ native Postgres bound to the default host port 5432, which silently
147
+ collides with `docker-compose.yml`'s old `5432:5432` mapping (you'd connect
148
+ to the wrong database with no error). Moving the compose Postgres's
149
+ *host-side* port to 55432 sidesteps this permanently; nothing about the
150
+ container's internal networking changes, so the `agent-comms-mcp`
151
+ service's own `DATABASE_URL` (which reaches `postgres` by service name on
152
+ the internal port 5432) is unaffected.
153
+
154
+ After starting Postgres, apply migrations before running the service or
155
+ the real-database tests:
156
+
157
+ ```bash
158
+ docker compose up -d postgres # start Postgres only (host port 55432)
159
+ export DATABASE_URL=postgresql://postgres:postgres@localhost:55432/agent_comms
160
+ uv run alembic upgrade head # create/upgrade the 5-table schema
161
+ ```
162
+
163
+ If you still hit a conflict (e.g. something else is bound to 55432), check
164
+ with `lsof -i :55432` and either free the port or change the host-side
165
+ number in `docker-compose.yml`'s `ports:` mapping for the `postgres`
166
+ service (updating `DATABASE_URL` to match) — a single fixed alternate port
167
+ is enough here, so there's no compose-override or env-var indirection.
168
+
169
+ To generate a new migration after changing `models.py`:
170
+
171
+ ```bash
172
+ uv run alembic revision --autogenerate -m "<description>"
173
+ ```
174
+
175
+ `tests/test_db_models.py` (and the other real-database test modules) run
176
+ against this same real Postgres instance (no mocking, per the RH standard)
177
+ and skip gracefully with a clear reason if they can't connect.
178
+
179
+ Configuration is env-driven and **fail-fast**: the service refuses to start
180
+ if any required variable (`OKTA_ISSUER_URL`, `OKTA_CLIENT_ID`,
181
+ `OKTA_CLIENT_SECRET`, `MCP_JWT_SECRET`, `AGENT_JWT_SECRET`, `DATABASE_URL`)
182
+ is missing or empty. See `.env.example` for the full list. No secrets are committed
183
+ anywhere in this repo.
184
+
185
+ ## Observability
186
+
187
+ Structured JSON logs via `structlog` to stdout. Events follow the schema in
188
+ `observability.py` (`tool_call`, `user_active`, `auth_flow`, `auth_rejected`,
189
+ `scope_denial`). Message content and attacker-controlled claim values are
190
+ never logged.
191
+
192
+ ## Deployment
193
+
194
+ The service is a standard Python HTTP process backed by PostgreSQL. The
195
+ included `Dockerfile` and `docker-compose.yml` cover local and self-hosted
196
+ deployments.
197
+
198
+ **Quick start (Docker Compose):**
199
+
200
+ ```bash
201
+ cp .env.example .env # fill in real values
202
+ docker compose up --build
203
+ ```
204
+
205
+ **Required environment variables** (see `.env.example`):
206
+
207
+ | Variable | Purpose |
208
+ |---|---|
209
+ | `OKTA_ISSUER_URL` | Okta OIDC issuer URL for interactive callers |
210
+ | `OKTA_CLIENT_ID` | Okta app client ID |
211
+ | `OKTA_CLIENT_SECRET` | Okta app client secret |
212
+ | `MCP_JWT_SECRET` | Signing secret for FastMCP's internal OAuth JWTs |
213
+ | `AGENT_JWT_SECRET` | Shared HS256 secret for agent JWT verification |
214
+ | `DATABASE_URL` | PostgreSQL connection string |
215
+
216
+ `entrypoint.sh` runs `alembic upgrade head` automatically on every container
217
+ start, so migrations apply before the server accepts traffic.
218
+
219
+ ## License
220
+
221
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,198 @@
1
+ # agent-comms-mcp
2
+
3
+ MCP service for **permissioned, structured agent-to-agent communications**.
4
+ First use case: a user's main agent delegates to a dedicated EA agent, which
5
+ communicates with other people's EA agents to negotiate availability (including
6
+ judgment, not just calendar overlap). Communications are scoped and structured
7
+ — no free text initially. See [`docs/DESIGN.md`](docs/DESIGN.md) for the full
8
+ spec (data model, permission model, message schemas). EA agent logic lives
9
+ elsewhere — this repo is only the comms layer.
10
+
11
+ ## Layout
12
+
13
+ ```
14
+ main.py # FastMCP server, observability + scope-enforcement middleware
15
+ auth.py # Okta OIDCProxy (humans) + agent-jwt JWTVerifier (agents) via MultiAuth
16
+ scopes.py # TOOL_SCOPES catalog + fail-closed scope helpers
17
+ identity.py # Issuer-gated JWT identity resolution (anti-impersonation guards)
18
+ observability.py # structlog JSON events (tool_call, scope_denial, auth_flow, ...)
19
+ providers/comms.py # Comms provider sub-server — the MCP tools (see below)
20
+ models.py # SQLAlchemy 2.x async ORM models (agents, conversations,
21
+ # participants, messages, audit_log — DESIGN.md §5)
22
+ db.py # Async engine/session factory (DATABASE_URL, fail-fast)
23
+ schemas.py # Pydantic message-payload schemas (all registered message types)
24
+ state_machine.py # Conversation/participant state transitions (DESIGN.md §4, §6)
25
+ service.py # Domain/service layer: membership rules, uniform denials, audit
26
+ exceptions.py # Service-layer exception shapes (mapped to ToolError in providers/comms.py)
27
+ migrations/ # Alembic migrations (async env.py); run `alembic upgrade head`
28
+ tests/ # pytest suite (composition, scope fail-closed, domain logic, schema)
29
+ ```
30
+
31
+ ## Domain layer
32
+
33
+ The comms board is five Postgres tables — `agents`, `conversations`,
34
+ `participants`, `messages`, `audit_log` — with `messages` and
35
+ `audit_log` append-only. An agent self-provisions via `comms_register`,
36
+ then either starts a conversation (adding named targets as `invited`) or
37
+ gets invited into one. A target only gains message-history read/write
38
+ access after calling `comms_accept` (`invited → active`); declining
39
+ (`comms_decline_invite`) is terminal and grants nothing. Task coordination
40
+ uses task message types (`task_assign`, `task_report`, `task_complete`,
41
+ `task_decline`, `task_cancel`) within ordinary conversations — task state
42
+ lives on `conversations.state`, not a separate table. Conversation types
43
+ (`open`, `internal`, `asymmetric`) gate admission by ownership; message
44
+ types gate boundary crossing via the `boundary_safe` flag — see
45
+ [`docs/DESIGN.md`](docs/DESIGN.md) §4–§9 for full details.
46
+
47
+ ## MCP tool surface
48
+
49
+ All tools below are mounted under the `comms` namespace (e.g. `whoami` in
50
+ `providers/comms.py` is exposed as `comms_whoami`) and enrolled in the
51
+ fail-closed `scopes.TOOL_SCOPES` registry. Source of truth:
52
+ `providers/comms.py`.
53
+
54
+ | Tool | Scope | Purpose |
55
+ |---|---|---|
56
+ | `comms_whoami` | `comms:read` | Return the caller's identity, issuer, caller type, and scopes |
57
+ | `comms_register` | `comms:write` | Idempotently self-provision (or re-bind) the caller's board `Agent` row |
58
+ | `comms_list_agents` | `comms:read` | Paginated board directory |
59
+ | `comms_start_conversation` | `comms:write` | Open a conversation with N target agents and post the seq-1 message |
60
+ | `comms_post_message` | `comms:write` | Post a typed, schema-validated message to an active conversation |
61
+ | `comms_get_conversation` | `comms:read` | Combined read: conversation + participants + messages since a seq; advances the caller's read cursor |
62
+ | `comms_inbox` | `comms:read` | Active conversations with unread messages, plus pending invites |
63
+ | `comms_list_conversations` | `comms:read` | Paginated list, filterable by role/type/state; newest-first |
64
+ | `comms_accept` | `comms:write` | Flip the caller's participant status `invited → active`, granting history read + posting rights |
65
+ | `comms_decline_invite` | `comms:write` | Decline a pending invite — terminal, no access is ever granted |
66
+ | `comms_invite` | `comms:write` | Invite another board agent into an active conversation (as `invited`) |
67
+ | `comms_leave` | `comms:write` | Leave a conversation the caller is currently `active` in |
68
+
69
+ ## Auth model
70
+
71
+ Both humans and machines POST to the same `/mcp` endpoint; FastMCP
72
+ `MultiAuth` routes them (`/health` is unauthenticated):
73
+
74
+ - **Humans** (Claude Code / Claude Desktop / browser): Okta OIDC via FastMCP
75
+ `OIDCProxy`. Identity claims (email) are available to tools via
76
+ `get_access_token().claims`. Interactive callers bypass per-tool scope
77
+ checks.
78
+ - **Agents / services**: HS256 Bearer JWT with `iss="agent-jwt"`, `sub`, and
79
+ `scopes` claims, verified by a `JWTVerifier` keyed to `AGENT_JWT_SECRET`.
80
+ Every tool call is then gated by the `TOOL_SCOPES` catalog in `scopes.py`
81
+ — **fail-closed**: a tool without a registry entry rejects every agent
82
+ call, denial messages are uniform (anti-enumeration), and each denial emits
83
+ a structured `scope_denial` log event.
84
+
85
+ When adding a tool, enroll its mounted name (`comms_<tool>`) in
86
+ `TOOL_SCOPES` in the same PR — `tests/test_main.py` fails otherwise.
87
+
88
+ ## Local development
89
+
90
+ Requires [uv](https://docs.astral.sh/uv/).
91
+
92
+ ```bash
93
+ uv sync # install deps from uv.lock
94
+
95
+ # Start Postgres, apply migrations, then run the tests (see "Database /
96
+ # migrations" below for why the port is 55432, not 5432)
97
+ docker compose up -d postgres
98
+ export DATABASE_URL=postgresql://postgres:postgres@localhost:55432/agent_comms
99
+ uv run alembic upgrade head
100
+ uv run pytest # tests
101
+ uv run ruff check . && uv run ruff format --check .
102
+ uv run mypy . # strict type check
103
+
104
+ # Run the server (needs real Okta + secret config)
105
+ cp .env.example .env # fill in values; .env is gitignored
106
+ uv run python main.py # http://127.0.0.1:8080/mcp
107
+
108
+ # Or the full stack (server + Postgres) in Docker
109
+ docker compose up --build
110
+ ```
111
+
112
+ Tests never touch the network: the Okta OIDC discovery call is patched out
113
+ in every test module that imports `main` (see `tests/test_main.py`'s
114
+ `_OIDC_PATCH`), so `uv run pytest` needs no real Okta tenant, issuer
115
+ reachability, or credentials — only a reachable Postgres for the
116
+ real-database tests (below), which skip cleanly if it's absent.
117
+
118
+ ### Database / migrations
119
+
120
+ Postgres is provisioned by `docker-compose.yml`, mapped to **host port
121
+ 55432** (container-internal port stays the standard 5432). This dev
122
+ machine — and, per earlier build stages, others too — already runs a
123
+ native Postgres bound to the default host port 5432, which silently
124
+ collides with `docker-compose.yml`'s old `5432:5432` mapping (you'd connect
125
+ to the wrong database with no error). Moving the compose Postgres's
126
+ *host-side* port to 55432 sidesteps this permanently; nothing about the
127
+ container's internal networking changes, so the `agent-comms-mcp`
128
+ service's own `DATABASE_URL` (which reaches `postgres` by service name on
129
+ the internal port 5432) is unaffected.
130
+
131
+ After starting Postgres, apply migrations before running the service or
132
+ the real-database tests:
133
+
134
+ ```bash
135
+ docker compose up -d postgres # start Postgres only (host port 55432)
136
+ export DATABASE_URL=postgresql://postgres:postgres@localhost:55432/agent_comms
137
+ uv run alembic upgrade head # create/upgrade the 5-table schema
138
+ ```
139
+
140
+ If you still hit a conflict (e.g. something else is bound to 55432), check
141
+ with `lsof -i :55432` and either free the port or change the host-side
142
+ number in `docker-compose.yml`'s `ports:` mapping for the `postgres`
143
+ service (updating `DATABASE_URL` to match) — a single fixed alternate port
144
+ is enough here, so there's no compose-override or env-var indirection.
145
+
146
+ To generate a new migration after changing `models.py`:
147
+
148
+ ```bash
149
+ uv run alembic revision --autogenerate -m "<description>"
150
+ ```
151
+
152
+ `tests/test_db_models.py` (and the other real-database test modules) run
153
+ against this same real Postgres instance (no mocking, per the RH standard)
154
+ and skip gracefully with a clear reason if they can't connect.
155
+
156
+ Configuration is env-driven and **fail-fast**: the service refuses to start
157
+ if any required variable (`OKTA_ISSUER_URL`, `OKTA_CLIENT_ID`,
158
+ `OKTA_CLIENT_SECRET`, `MCP_JWT_SECRET`, `AGENT_JWT_SECRET`, `DATABASE_URL`)
159
+ is missing or empty. See `.env.example` for the full list. No secrets are committed
160
+ anywhere in this repo.
161
+
162
+ ## Observability
163
+
164
+ Structured JSON logs via `structlog` to stdout. Events follow the schema in
165
+ `observability.py` (`tool_call`, `user_active`, `auth_flow`, `auth_rejected`,
166
+ `scope_denial`). Message content and attacker-controlled claim values are
167
+ never logged.
168
+
169
+ ## Deployment
170
+
171
+ The service is a standard Python HTTP process backed by PostgreSQL. The
172
+ included `Dockerfile` and `docker-compose.yml` cover local and self-hosted
173
+ deployments.
174
+
175
+ **Quick start (Docker Compose):**
176
+
177
+ ```bash
178
+ cp .env.example .env # fill in real values
179
+ docker compose up --build
180
+ ```
181
+
182
+ **Required environment variables** (see `.env.example`):
183
+
184
+ | Variable | Purpose |
185
+ |---|---|
186
+ | `OKTA_ISSUER_URL` | Okta OIDC issuer URL for interactive callers |
187
+ | `OKTA_CLIENT_ID` | Okta app client ID |
188
+ | `OKTA_CLIENT_SECRET` | Okta app client secret |
189
+ | `MCP_JWT_SECRET` | Signing secret for FastMCP's internal OAuth JWTs |
190
+ | `AGENT_JWT_SECRET` | Shared HS256 secret for agent JWT verification |
191
+ | `DATABASE_URL` | PostgreSQL connection string |
192
+
193
+ `entrypoint.sh` runs `alembic upgrade head` automatically on every container
194
+ start, so migrations apply before the server accepts traffic.
195
+
196
+ ## License
197
+
198
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,221 @@
1
+ Metadata-Version: 2.4
2
+ Name: agent-comms-mcp
3
+ Version: 0.1.0
4
+ Summary: MCP service for permissioned, structured agent-to-agent communications
5
+ Author-email: Redesign Health <tech@redesignhealth.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/redesignhealth/agent-comms-mcp
8
+ Project-URL: Repository, https://github.com/redesignhealth/agent-comms-mcp
9
+ Project-URL: Issues, https://github.com/redesignhealth/agent-comms-mcp/issues
10
+ Requires-Python: >=3.12
11
+ Description-Content-Type: text/markdown
12
+ License-File: LICENSE
13
+ Requires-Dist: alembic==1.18.5
14
+ Requires-Dist: asyncpg==0.31.0
15
+ Requires-Dist: fastmcp==3.4.2
16
+ Requires-Dist: greenlet==3.5.4
17
+ Requires-Dist: httpx==0.28.1
18
+ Requires-Dist: py-key-value-aio[filetree]==0.4.5
19
+ Requires-Dist: pydantic==2.12.5
20
+ Requires-Dist: sqlalchemy[asyncio]==2.0.51
21
+ Requires-Dist: structlog==25.5.0
22
+ Dynamic: license-file
23
+
24
+ # agent-comms-mcp
25
+
26
+ MCP service for **permissioned, structured agent-to-agent communications**.
27
+ First use case: a user's main agent delegates to a dedicated EA agent, which
28
+ communicates with other people's EA agents to negotiate availability (including
29
+ judgment, not just calendar overlap). Communications are scoped and structured
30
+ — no free text initially. See [`docs/DESIGN.md`](docs/DESIGN.md) for the full
31
+ spec (data model, permission model, message schemas). EA agent logic lives
32
+ elsewhere — this repo is only the comms layer.
33
+
34
+ ## Layout
35
+
36
+ ```
37
+ main.py # FastMCP server, observability + scope-enforcement middleware
38
+ auth.py # Okta OIDCProxy (humans) + agent-jwt JWTVerifier (agents) via MultiAuth
39
+ scopes.py # TOOL_SCOPES catalog + fail-closed scope helpers
40
+ identity.py # Issuer-gated JWT identity resolution (anti-impersonation guards)
41
+ observability.py # structlog JSON events (tool_call, scope_denial, auth_flow, ...)
42
+ providers/comms.py # Comms provider sub-server — the MCP tools (see below)
43
+ models.py # SQLAlchemy 2.x async ORM models (agents, conversations,
44
+ # participants, messages, audit_log — DESIGN.md §5)
45
+ db.py # Async engine/session factory (DATABASE_URL, fail-fast)
46
+ schemas.py # Pydantic message-payload schemas (all registered message types)
47
+ state_machine.py # Conversation/participant state transitions (DESIGN.md §4, §6)
48
+ service.py # Domain/service layer: membership rules, uniform denials, audit
49
+ exceptions.py # Service-layer exception shapes (mapped to ToolError in providers/comms.py)
50
+ migrations/ # Alembic migrations (async env.py); run `alembic upgrade head`
51
+ tests/ # pytest suite (composition, scope fail-closed, domain logic, schema)
52
+ ```
53
+
54
+ ## Domain layer
55
+
56
+ The comms board is five Postgres tables — `agents`, `conversations`,
57
+ `participants`, `messages`, `audit_log` — with `messages` and
58
+ `audit_log` append-only. An agent self-provisions via `comms_register`,
59
+ then either starts a conversation (adding named targets as `invited`) or
60
+ gets invited into one. A target only gains message-history read/write
61
+ access after calling `comms_accept` (`invited → active`); declining
62
+ (`comms_decline_invite`) is terminal and grants nothing. Task coordination
63
+ uses task message types (`task_assign`, `task_report`, `task_complete`,
64
+ `task_decline`, `task_cancel`) within ordinary conversations — task state
65
+ lives on `conversations.state`, not a separate table. Conversation types
66
+ (`open`, `internal`, `asymmetric`) gate admission by ownership; message
67
+ types gate boundary crossing via the `boundary_safe` flag — see
68
+ [`docs/DESIGN.md`](docs/DESIGN.md) §4–§9 for full details.
69
+
70
+ ## MCP tool surface
71
+
72
+ All tools below are mounted under the `comms` namespace (e.g. `whoami` in
73
+ `providers/comms.py` is exposed as `comms_whoami`) and enrolled in the
74
+ fail-closed `scopes.TOOL_SCOPES` registry. Source of truth:
75
+ `providers/comms.py`.
76
+
77
+ | Tool | Scope | Purpose |
78
+ |---|---|---|
79
+ | `comms_whoami` | `comms:read` | Return the caller's identity, issuer, caller type, and scopes |
80
+ | `comms_register` | `comms:write` | Idempotently self-provision (or re-bind) the caller's board `Agent` row |
81
+ | `comms_list_agents` | `comms:read` | Paginated board directory |
82
+ | `comms_start_conversation` | `comms:write` | Open a conversation with N target agents and post the seq-1 message |
83
+ | `comms_post_message` | `comms:write` | Post a typed, schema-validated message to an active conversation |
84
+ | `comms_get_conversation` | `comms:read` | Combined read: conversation + participants + messages since a seq; advances the caller's read cursor |
85
+ | `comms_inbox` | `comms:read` | Active conversations with unread messages, plus pending invites |
86
+ | `comms_list_conversations` | `comms:read` | Paginated list, filterable by role/type/state; newest-first |
87
+ | `comms_accept` | `comms:write` | Flip the caller's participant status `invited → active`, granting history read + posting rights |
88
+ | `comms_decline_invite` | `comms:write` | Decline a pending invite — terminal, no access is ever granted |
89
+ | `comms_invite` | `comms:write` | Invite another board agent into an active conversation (as `invited`) |
90
+ | `comms_leave` | `comms:write` | Leave a conversation the caller is currently `active` in |
91
+
92
+ ## Auth model
93
+
94
+ Both humans and machines POST to the same `/mcp` endpoint; FastMCP
95
+ `MultiAuth` routes them (`/health` is unauthenticated):
96
+
97
+ - **Humans** (Claude Code / Claude Desktop / browser): Okta OIDC via FastMCP
98
+ `OIDCProxy`. Identity claims (email) are available to tools via
99
+ `get_access_token().claims`. Interactive callers bypass per-tool scope
100
+ checks.
101
+ - **Agents / services**: HS256 Bearer JWT with `iss="agent-jwt"`, `sub`, and
102
+ `scopes` claims, verified by a `JWTVerifier` keyed to `AGENT_JWT_SECRET`.
103
+ Every tool call is then gated by the `TOOL_SCOPES` catalog in `scopes.py`
104
+ — **fail-closed**: a tool without a registry entry rejects every agent
105
+ call, denial messages are uniform (anti-enumeration), and each denial emits
106
+ a structured `scope_denial` log event.
107
+
108
+ When adding a tool, enroll its mounted name (`comms_<tool>`) in
109
+ `TOOL_SCOPES` in the same PR — `tests/test_main.py` fails otherwise.
110
+
111
+ ## Local development
112
+
113
+ Requires [uv](https://docs.astral.sh/uv/).
114
+
115
+ ```bash
116
+ uv sync # install deps from uv.lock
117
+
118
+ # Start Postgres, apply migrations, then run the tests (see "Database /
119
+ # migrations" below for why the port is 55432, not 5432)
120
+ docker compose up -d postgres
121
+ export DATABASE_URL=postgresql://postgres:postgres@localhost:55432/agent_comms
122
+ uv run alembic upgrade head
123
+ uv run pytest # tests
124
+ uv run ruff check . && uv run ruff format --check .
125
+ uv run mypy . # strict type check
126
+
127
+ # Run the server (needs real Okta + secret config)
128
+ cp .env.example .env # fill in values; .env is gitignored
129
+ uv run python main.py # http://127.0.0.1:8080/mcp
130
+
131
+ # Or the full stack (server + Postgres) in Docker
132
+ docker compose up --build
133
+ ```
134
+
135
+ Tests never touch the network: the Okta OIDC discovery call is patched out
136
+ in every test module that imports `main` (see `tests/test_main.py`'s
137
+ `_OIDC_PATCH`), so `uv run pytest` needs no real Okta tenant, issuer
138
+ reachability, or credentials — only a reachable Postgres for the
139
+ real-database tests (below), which skip cleanly if it's absent.
140
+
141
+ ### Database / migrations
142
+
143
+ Postgres is provisioned by `docker-compose.yml`, mapped to **host port
144
+ 55432** (container-internal port stays the standard 5432). This dev
145
+ machine — and, per earlier build stages, others too — already runs a
146
+ native Postgres bound to the default host port 5432, which silently
147
+ collides with `docker-compose.yml`'s old `5432:5432` mapping (you'd connect
148
+ to the wrong database with no error). Moving the compose Postgres's
149
+ *host-side* port to 55432 sidesteps this permanently; nothing about the
150
+ container's internal networking changes, so the `agent-comms-mcp`
151
+ service's own `DATABASE_URL` (which reaches `postgres` by service name on
152
+ the internal port 5432) is unaffected.
153
+
154
+ After starting Postgres, apply migrations before running the service or
155
+ the real-database tests:
156
+
157
+ ```bash
158
+ docker compose up -d postgres # start Postgres only (host port 55432)
159
+ export DATABASE_URL=postgresql://postgres:postgres@localhost:55432/agent_comms
160
+ uv run alembic upgrade head # create/upgrade the 5-table schema
161
+ ```
162
+
163
+ If you still hit a conflict (e.g. something else is bound to 55432), check
164
+ with `lsof -i :55432` and either free the port or change the host-side
165
+ number in `docker-compose.yml`'s `ports:` mapping for the `postgres`
166
+ service (updating `DATABASE_URL` to match) — a single fixed alternate port
167
+ is enough here, so there's no compose-override or env-var indirection.
168
+
169
+ To generate a new migration after changing `models.py`:
170
+
171
+ ```bash
172
+ uv run alembic revision --autogenerate -m "<description>"
173
+ ```
174
+
175
+ `tests/test_db_models.py` (and the other real-database test modules) run
176
+ against this same real Postgres instance (no mocking, per the RH standard)
177
+ and skip gracefully with a clear reason if they can't connect.
178
+
179
+ Configuration is env-driven and **fail-fast**: the service refuses to start
180
+ if any required variable (`OKTA_ISSUER_URL`, `OKTA_CLIENT_ID`,
181
+ `OKTA_CLIENT_SECRET`, `MCP_JWT_SECRET`, `AGENT_JWT_SECRET`, `DATABASE_URL`)
182
+ is missing or empty. See `.env.example` for the full list. No secrets are committed
183
+ anywhere in this repo.
184
+
185
+ ## Observability
186
+
187
+ Structured JSON logs via `structlog` to stdout. Events follow the schema in
188
+ `observability.py` (`tool_call`, `user_active`, `auth_flow`, `auth_rejected`,
189
+ `scope_denial`). Message content and attacker-controlled claim values are
190
+ never logged.
191
+
192
+ ## Deployment
193
+
194
+ The service is a standard Python HTTP process backed by PostgreSQL. The
195
+ included `Dockerfile` and `docker-compose.yml` cover local and self-hosted
196
+ deployments.
197
+
198
+ **Quick start (Docker Compose):**
199
+
200
+ ```bash
201
+ cp .env.example .env # fill in real values
202
+ docker compose up --build
203
+ ```
204
+
205
+ **Required environment variables** (see `.env.example`):
206
+
207
+ | Variable | Purpose |
208
+ |---|---|
209
+ | `OKTA_ISSUER_URL` | Okta OIDC issuer URL for interactive callers |
210
+ | `OKTA_CLIENT_ID` | Okta app client ID |
211
+ | `OKTA_CLIENT_SECRET` | Okta app client secret |
212
+ | `MCP_JWT_SECRET` | Signing secret for FastMCP's internal OAuth JWTs |
213
+ | `AGENT_JWT_SECRET` | Shared HS256 secret for agent JWT verification |
214
+ | `DATABASE_URL` | PostgreSQL connection string |
215
+
216
+ `entrypoint.sh` runs `alembic upgrade head` automatically on every container
217
+ start, so migrations apply before the server accepts traffic.
218
+
219
+ ## License
220
+
221
+ MIT — see [LICENSE](LICENSE).