Thin runtime for the squad (alex, anna, garth, joanne, ross, tim): every inbound message is handled as a bounded multi-step task by mudler/cogito against tools/list from skills-mcp-server, plus a built-in direct_reply tool (no MCP). The router may chain MCP tools (for example read_web then create_google_doc) before finishing with direct_reply. Ordering is model-chosen within the step cap, with one narrow deterministic exception: research-heavy Google Doc follow-ups (title/editor + paper/pages/research cues without repeating “google doc”) trigger a single injected read_web before the first model step so thread-only text still gets grounding.
JetStream consumes slack.work.*.events (one subject per employee, e.g. slack.work.joanne.events). Set each employee’s *_SLACK_BOT_TOKEN so NATS replies post as the right bot.
The process exits at startup unless:
GEMINI_API_KEYand/orAGENTS_ROUTER_API_KEYresolves to a non-empty router key (see below), andAGENTS_ROUTER_DISABLEDis not true.
tools/listfromskills-mcp-server→ register MCP tools +direct_replyas OpenAI-style tools.- Loop (max
AGENTS_ROUTER_MAX_TOOL_STEPS, default 8, hard cap 20): each iteration is one OpenAI-compatiblechat/completionscall withtool_choice: required(Gemini default base URL when usingGEMINI_API_KEYonly). direct_reply→ render straight to Slack and stop. Any other tool →tools/callon MCP; the tool result JSON is appended to the chat for the next iteration so the model can plan the following step.- On the last iteration, the server appends a short user nudge and sets
tool_choicetodirect_replyonly so the task always ends with a Slack-visible message.
State: in-memory thread store only. Optional lightweight local debug UI at /.
| Priority | Env | Role |
|---|---|---|
| 1 | AGENTS_ROUTER_API_KEY |
OpenAI-compatible API key when set (optional if Gemini key exists). |
| 2 | GEMINI_API_KEY |
Used when AGENTS_ROUTER_API_KEY is empty. Defaults AGENTS_ROUTER_BASE_URL to https://generativelanguage.googleapis.com/v1beta/openai/ and AGENTS_ROUTER_MODEL from GEMINI_MODEL (default gemini-2.5-flash). |
| Override | AGENTS_ROUTER_BASE_URL, AGENTS_ROUTER_MODEL |
Force provider/model regardless of source key. |
OpenAI-style routing without Gemini: set AGENTS_ROUTER_API_KEY and optionally AGENTS_ROUTER_BASE_URL / AGENTS_ROUTER_MODEL (defaults to gpt-4o-mini when not using the Gemini-only resolution above).
Copy .env.dev.example → .env.dev in each repo that participates in the stack (gitignored):
| Repo | File |
|---|---|
agents-mcp-server |
.env.dev |
skills-mcp-server |
.env.dev |
slack-orchestrator |
.env.dev |
makeacompany-ai |
.env.dev |
agents-mcp-server keys include:
SKILLS_MCP_URL— MCP endpoint forskills-mcp-server(…/mcp).AGENTS_NATS_URL— when set, starts a JetStream push consumer onslack.work.*.events(same stream asslack-orchestrator). Compose setsnats://nats:4222for the full stack.AGENTS_NATS_CONSUME_NEW_ONLY— defaulttrue; consume only newly published JetStream events (prevents old backlog replay on restart).AGENTS_NATS_MAX_EVENT_AGE_SEC— default300; drops stale events older than this threshold by JetStream publish time and Slackmessage_ts.AGENTS_ROUTER_MAX_TOOL_STEPS— default8; max tool invocations per user message (includingdirect_reply). Capped at20.AGENTS_SLACK_POST_FROM_NATS— default true; when false, inbound NATS turns still run the router but skipchat.postMessage.ALEX_SLACK_*,ANNA_SLACK_*,GARTH_SLACK_*,JOANNE_SLACK_*,ROSS_SLACK_*,TIM_SLACK_*— per-employee bot (+ optional app) tokens for JetStream thread replies. CheckGET /api/status(slackEnvPresent,natsEnabled,routerModel,routerKeySource).
Per-service env files: docker-compose.yml uses each sibling repo’s .env.dev via env_file (no Compose ${VAR} interpolation). docker-compose.yml still overrides ORCHESTRATOR_NATS_URL, ORCHESTRATOR_DISPATCH_ENABLED, and ORCHESTRATOR_TERMS_REDIS_URL on the orchestrator container so local JetStream targets the compose NATS broker and terms gate stays off unless you set Redis in slack-orchestrator/.env.dev.
Joanne #humans terms gate: omit ORCHESTRATOR_TERMS_REDIS_URL (compose sets it empty) so all human messages route without acceptance checks. Set Redis only when you want prod-like terms enforcement.
From this directory:
cp .env.dev.example .env.dev
cp ../skills-mcp-server/.env.dev.example ../skills-mcp-server/.env.dev
cp ../slack-orchestrator/.env.dev.example ../slack-orchestrator/.env.dev
cp ../makeacompany-ai/.env.dev.example ../makeacompany-ai/.env.dev
# Min: GEMINI_* in skills + agents for router; per-employee Slack tokens in agents; orchestrator tokens in slack-orchestrator; Stripe/Google/Resend in makeacompany-ai as needed.
docker compose --profile local up --buildServices (profile local):
| Service | Port | Role |
|---|---|---|
skills-mcp-server |
8081 | MCP tools (read_web, skill FS tools, …) |
agents-mcp-server |
8090 | HTTP /api/chat, JetStream consumer on slack.work.*.events, optional Slack thread replies |
nats |
4222 (8222 monitoring) | JetStream broker shared with slack-orchestrator |
slack-orchestrator |
8080 | Slack ingress → NATS only (compose disables Tier-1 / semantic tool routing; routing stays in agents + MCP) |
Without NATS, run only skills + agents and unset AGENTS_NATS_URL (compose sets it when using the full stack).
Each app keeps production material in its own .env.prod (gitignored). Sync is runtime-only (Secrets in the cluster); GitOps image pins stay on tag-driven CI per rancher-admin.
| App | Repo | Command (from that repo) | Secret / namespace |
|---|---|---|---|
| agents-mcp-server | this repo | ./scripts/update-rancher-secrets.sh |
agents-mcp-server-runtime / agents-mcp-server |
| skills-mcp-server | skills-mcp-server |
./scripts/update-rancher-secrets.sh |
skills-mcp-server-runtime / skills-mcp-server |
| slack-orchestrator | slack-orchestrator |
./scripts/update-rancher-secrets.sh |
slack-orchestrator-runtime / slack-orchestrator |
| makeacompany-ai | makeacompany-ai |
./scripts/update-rancher-secrets.sh |
makeacompany-ai-runtime-secrets / makeacompany-ai |
Use export KUBECONFIG="${HOME}/.kube/config/admin.yaml" and kubectl --context admin … when your default context is not admin.
Verify after apply: kubectl --context admin -n <ns> get secret <name> -o jsonpath='{.data}' only shows base64 key names; confirm pods pick up changes with kubectl --context admin -n <ns> rollout status deploy/<deploy> or ROLLOUT_RESTART=true where the script supports it (agents-mcp-server, slack-orchestrator; skills-mcp-server script restarts its deployment by default; makeacompany-ai restarts backend+frontend by default).
Cross-service consistency: e.g. BACKEND_INTERNAL_SERVICE_TOKEN / AGENT_FACTORY_ADMIN_TOKEN must match what makeacompany-ai and employee-factory scripts document—rotating one surface without the others yields 401s, not a broken kubectl apply.
GET /healthGET /api/statusPOST /api/chat
Example:
curl -s http://localhost:8090/api/chat \
-H "content-type: application/json" \
-d '{"agent":"ross","message":"latest updates on model context protocol standard","threadId":"demo"}'