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.
- agent_comms_mcp-0.1.0/LICENSE +21 -0
- agent_comms_mcp-0.1.0/PKG-INFO +221 -0
- agent_comms_mcp-0.1.0/README.md +198 -0
- agent_comms_mcp-0.1.0/agent_comms_mcp.egg-info/PKG-INFO +221 -0
- agent_comms_mcp-0.1.0/agent_comms_mcp.egg-info/SOURCES.txt +41 -0
- agent_comms_mcp-0.1.0/agent_comms_mcp.egg-info/dependency_links.txt +1 -0
- agent_comms_mcp-0.1.0/agent_comms_mcp.egg-info/entry_points.txt +2 -0
- agent_comms_mcp-0.1.0/agent_comms_mcp.egg-info/requires.txt +9 -0
- agent_comms_mcp-0.1.0/agent_comms_mcp.egg-info/top_level.txt +13 -0
- agent_comms_mcp-0.1.0/auth.py +343 -0
- agent_comms_mcp-0.1.0/db.py +70 -0
- agent_comms_mcp-0.1.0/exceptions.py +107 -0
- agent_comms_mcp-0.1.0/identity.py +102 -0
- agent_comms_mcp-0.1.0/main.py +305 -0
- agent_comms_mcp-0.1.0/migrations/env.py +100 -0
- agent_comms_mcp-0.1.0/migrations/versions/15ef34885e30_add_conversations_owner_snapshot.py +65 -0
- agent_comms_mcp-0.1.0/migrations/versions/18f2d7735523_rate_limit_indexes_and_display_name_.py +124 -0
- agent_comms_mcp-0.1.0/migrations/versions/6d2a8e63e469_add_tasks_table.py +127 -0
- agent_comms_mcp-0.1.0/migrations/versions/da3e1646c44d_drop_tasks_table.py +194 -0
- agent_comms_mcp-0.1.0/migrations/versions/e1db7c2e6b70_backfill_accepted_types_for_existing_.py +92 -0
- agent_comms_mcp-0.1.0/migrations/versions/ef8394b37c8d_initial_schema.py +190 -0
- agent_comms_mcp-0.1.0/models.py +232 -0
- agent_comms_mcp-0.1.0/observability.py +245 -0
- agent_comms_mcp-0.1.0/providers/__init__.py +6 -0
- agent_comms_mcp-0.1.0/providers/comms.py +755 -0
- agent_comms_mcp-0.1.0/pyproject.toml +85 -0
- agent_comms_mcp-0.1.0/schemas.py +544 -0
- agent_comms_mcp-0.1.0/scopes.py +170 -0
- agent_comms_mcp-0.1.0/service.py +2264 -0
- agent_comms_mcp-0.1.0/setup.cfg +4 -0
- agent_comms_mcp-0.1.0/state_machine.py +143 -0
- agent_comms_mcp-0.1.0/tests/test_auth.py +426 -0
- agent_comms_mcp-0.1.0/tests/test_comms_tools.py +1582 -0
- agent_comms_mcp-0.1.0/tests/test_db_models.py +297 -0
- agent_comms_mcp-0.1.0/tests/test_identity.py +145 -0
- agent_comms_mcp-0.1.0/tests/test_main.py +522 -0
- agent_comms_mcp-0.1.0/tests/test_migrations_offline.py +80 -0
- agent_comms_mcp-0.1.0/tests/test_observability.py +205 -0
- agent_comms_mcp-0.1.0/tests/test_schemas.py +537 -0
- agent_comms_mcp-0.1.0/tests/test_scopes.py +187 -0
- agent_comms_mcp-0.1.0/tests/test_service.py +3030 -0
- agent_comms_mcp-0.1.0/tests/test_state_machine.py +185 -0
- 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).
|