Skip to content

feat(chat)!: serve the AG-UI chat instead of Chainlit - #37

Merged
ciaransweet merged 6 commits into
mainfrom
feat/agui-chat
Sep 9, 2026
Merged

feat(chat)!: serve the AG-UI chat instead of Chainlit#37
ciaransweet merged 6 commits into
mainfrom
feat/agui-chat

Conversation

@ciaransweet

Copy link
Copy Markdown
Contributor

Closes #31.

The hosted chat was the runtime's Chainlit host, which showed a conversation and nothing else. Everything this repository exists to demonstrate — where a tool's arguments came from, what the toolsets exchanged without the model reading it, a tool's own view — happened where nobody could see it.

It is now mcp_agent_api: the agent over HTTP as AG-UI events, and the web client that renders them, both inside the runtime wheel as of 0.8.4. The image installs one package and runs uvicorn — no node stage, no vendored frontend, no install-elements after a runtime bump. Chainlit is gone from this repo entirely, the root pin included.

The model is now the deployment's

Chainlit was bring-your-own-model because it had a settings dialog to type into. This agent is built once at startup, so whoever opens the page spends the deployment's key. Rather than quietly hold one, neither target deploys the chat without one — a repository made from this template has no chat until someone puts a key there deliberately.

Turns it on Where the key lives
Kubernetes MCP_PROVIDER_API_KEY secret a Secret this chart templates, or one you manage via provider.existingSecret
AWS MCP_AWS_CHAT_MODEL variable Parameter Store SecureString at /mcp-toolsets/<instance>/chat/provider-api-key

The key is passed with --set-file on the Kubernetes side, because helm reads a comma in a --set value as a separator and would truncate a key into something that looks merely rejected. On AWS it is never CDK context, which would put it in the CloudFormation template.

Per-toolset credentials survive the move. The runtime's new GET /connections reports which headers the toolsets declared, the page offers a field per header, and a visitor's own value beats the deployment's.

Two things found by wiring it up

  • A DNS record was written for a chat that is not deployed. A hostname resolving to the load balancer with no rule behind it lands on the index, so the chat host answers with the directory and reads as a broken chat. One predicate now answers the three questions that have to agree: the certificate's second name, the listener rule, and the record.
  • The AWS health check was on /. The page is served as soon as the process is up and cannot answer until it has connected to every toolset, so a task joined the load balancer while every question it got would 503. It is /health/readiness now.

Checks

./scripts/lint, ./scripts/test (35), the CDK tests (43, four of them new), helm lint on the chat chart — now part of CI — and a synth both ways confirming the chat appears with a model and no chat resources or record appear without one, with an empty missing in the manifest either way.

🤖 Generated with Claude Code

ciaransweet and others added 6 commits September 8, 2026 22:57
Closes #31.

The hosted chat was the runtime's Chainlit host, which showed a conversation
and nothing else. Everything this repository exists to demonstrate — where a
tool's arguments came from, what the toolsets exchanged without the model
reading it, a tool's own view — happened where nobody could see it.

It is now `mcp_agent_api`: the agent over HTTP as AG-UI events, and the web
client that renders them, both inside the runtime wheel as of 0.8.4. So the
image installs one package and runs uvicorn — no node stage, no vendored
frontend, no `install-elements` step after a runtime bump. Chainlit is gone
from this repo entirely, the root pin included.

**The model is now the deployment's.** Chainlit was bring-your-own-model
because it had a settings dialog to type into; this agent is built once at
startup, so whoever opens the page spends the deployment's key. Rather than
quietly hold one, neither target deploys the chat without one:

- Kubernetes: the `MCP_PROVIDER_API_KEY` secret gates the job, and the chart
  templates a Secret from it (or takes one you manage). `--set-file`, because
  helm reads a comma in `--set` as a separator and would truncate a key.
- AWS: the `MCP_AWS_CHAT_MODEL` variable is the on switch, and the key is a
  Parameter Store SecureString read at task start — passing it as context
  would put it in the CloudFormation template.

Per-toolset credentials survive the move: the new `GET /connections` route
reports which headers the toolsets declared, the page offers a field per
header, and a visitor's value beats the deployment's.

Two things found while wiring it up rather than by reading:

A DNS record was written for a chat that is not deployed — a hostname that
resolves to the load balancer with no rule behind it lands on the index, so
the chat host answers with the directory. One predicate now answers all three
questions that have to agree: the certificate's second name, the listener rule
and the record.

The AWS health check was on `/`, which the old chat had no better answer for.
The page is served as soon as the process is up and stays unable to answer
until it has connected to every toolset, so the check is now `/health/readiness`
and a task joins the load balancer when it can actually answer.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Nothing writes .chainlit/, chainlit.md or public/elements any more: the chat
image runs uvicorn and the web client is inside the wheel. An ignore for a file
nothing produces is a reader's clue to code that is gone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The two targets gated on different halves of the same decision: Kubernetes on
the key, AWS on the model. Each then had a sharp edge where the other half was
missing — a key with no model failed inside `helm upgrade`, and a model with no
key started a task that could not run and rolled back. Neither said what to
set, and the asymmetry had no reason behind it beyond how each was written.

The model is now the switch on both, because it is the one input both can see:
a job-level `if` cannot read secrets, and synthesis is credential-free by rule,
so the stack cannot know whether a Parameter Store entry exists. What both
*can* do is check the key early and say so — Kubernetes reads the secret in a
first step, AWS reads the parameter's version without decrypting it, and each
error names the exact command to run or the variable to clear.

That leaves one decision with one switch. A repository made from this template
still deploys no chat until someone names a model, which was the point.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Reviewing the branch rather than reading it, starting from what a container
actually does at startup. uvicorn opens its socket *after* the lifespan, and
the lifespan is where the agent connects to every toolset — so during startup
this container refuses connections rather than answering 503, which is what
two comments here claimed and neither target was set up for.

- Kubernetes had a liveness probe and no startup probe, so its own failures
  would restart a container that was still connecting. A slow toolset would
  have produced a crash loop that reads as a broken image. There is a startup
  probe now, with the liveness and readiness checks behind it.
- ECS took the library's 60s grace period, which is right for a toolset that
  serves the moment it starts and thin for one that connects to every toolset
  first. The chat gets three minutes; nothing else changes.

Then four smaller ones, each found by trying it:

- A `ui.examples` entry spanning lines broke out of the block scalar and the
  whole Deployment failed to parse. Joined and quoted instead.
- `provider.existingSecret` with a blank `existingSecretKey` rendered
  `key:` with nothing after it. It is `required` now.
- `containerPort` followed `service.port`, so changing the Service's port
  pointed the probes and the Service at a port the image does not serve. The
  image fixes 8080; so does the container port, and the Service targets it by
  name.
- Both new error messages substituted a repository variable into the script
  body. They go through the environment now. The AWS one also told you to
  create a parameter whatever went wrong, including an expired token; it tells
  ParameterNotFound from the rest.

And two doc leaks of my own: a Kubernetes-only repo learned about
`MCP_AWS_CHAT_MODEL` and `infra/cdk`, because the sentence naming both targets
was outside any marker. Marked. (The comparison table and a few lines of prose
elsewhere leak the same way and predate this branch.)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A typo in MCP_AGENT_UI_TITLE fails silently and identically on both targets:
the deployment sets a variable nothing reads, the client falls back to its own
default, and the page looks merely unconfigured. Nothing here would have
noticed — a chart render and a stack synthesis both know what they set and
neither knows what the runtime is looking for.

The test reads the names out of `mcp_agent_api.ui` and asserts each target
names exactly those, so a variable that nothing reads fails as loudly as a
missing one. Text rather than imports, because `infra/cdk/config.py` needs the
`infra` group and the chart is YAML — one test covers both targets in the base
job, and follows whichever target survived a bootstrap.

Verified by breaking it both ways: a renamed variable in the chart and one in
the CDK config each fail it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…more

- Kubernetes: the reconcile job uninstalls mcp-chat when MCP_CHAT_MODEL is
  empty. The chat job was merely skipped, so a cleared variable left the
  release running and spending the key, while the AWS stack reconciled.
- AWS: the preflight fails on a model with no MCP_AWS_INGRESS_HOST, which
  previously deployed no chat and said nothing, and now hands the stack
  the parameter path it checked (chatApiKeyParameter), so the two agree.
- Chart: a model with no apiKey or existingSecret fails at install with a
  message, instead of a pod waiting forever on a Secret never written.
- .dockerignore: both Dockerfiles copy the whole tree into a builder stage;
  .venv, node_modules, cdk.out, .env and any key file no longer go with it.
- .example.env: drop the stale mcp-agent-web name and put the hosted-chat
  note under the two variables it describes.
- chat_host docstring states the rule rather than a bug main never had.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@ciaransweet
ciaransweet merged commit 03127a5 into main Sep 9, 2026
11 checks passed
@ciaransweet
ciaransweet deleted the feat/agui-chat branch September 9, 2026 09:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Swap out mcp-toolsets-runtime chainlit app for UI closer to AGUI example

1 participant