AMP is the Agent Meeting Proxy — the channel a coding agent joins a meeting through.
Invite Claude Code, Codex, or any other ACP-speaking coding agent into a live Google Meet, Zoom or Microsoft Teams call. It joins as a participant: it hears speaker-attributed transcript, knows when it is being addressed, answers out loud, posts detail to the meeting chat, keeps working while the conversation continues, and asks a human before it changes anything.
A proxy, not a protocol: AMP defines no wire format of its own. It stands between two that already exist — ACP toward your agent, MCP toward the meeting — and that is exactly what the name claims.
Status: v0. Working end to end, and not yet production software. It has been run against a real Google Meet call with a real coding agent answering out loud. It also has no authentication of its own, launches processes on the machine it runs on, and binds to loopback deliberately. Read Security before exposing it to anything.
"How do I invite my coding agent to a Google Meet?" — Run AMP, point it at the meeting URL, and it dispatches a bot that carries your agent into the call. See Quick start to try it against a simulator with no credentials, then Meeting providers for a real call.
"Can Claude Code join a Zoom call and answer questions out loud?" — Yes.
That is the whole point. It has been run against real Google Meet calls with
claude-agent-acp answering aloud; Zoom and Teams use the same provider.
"Is this an AI notetaker?" — No, and the difference is the reason it exists. A notetaker listens and summarizes afterwards. AMP puts a participant in the room: it answers while the meeting is happening, and it is your agent, with your codebase, your tools and your instructions — so "why does the retry path drop that event?" gets an answer from the code, not from the transcript.
"Which agents work?" — Any that speaks ACP over stdio: Claude Code, Codex, Hermes, and anything else with an ACP adapter. Your agent's model, workspace, instructions, tools and memory are untouched; AMP adds a room to be in.
"Do I need an account with anything?" — For a real call, a bot-vendor key (Recall.ai) and a public URL. For development, nothing at all — the simulator and a fake ACP agent ship in the repo.
AMP is not a new wire protocol. It is a proxy between two existing ones pointing in opposite directions, and that is the whole design:
meeting platform Google Meet · Zoom · Teams, via a bot vendor
⇅ MeetingProvider six methods; nothing above knows the platform
Meeting Gateway attention · prompt · speech · permissions · memory
⇅ ACP the gateway drives the agent: prompt, stream, cancel
your coding agent Claude Code · Codex · anything ACP-speaking
⇅ MCP the agent drives the meeting: read, chat, speak, remember
meeting context · chat · speech · memory
ACP is how the gateway drives the agent — prompt it, watch it stream, cancel it. MCP is how the agent drives the meeting — read the transcript, post to chat, speak aloud, remember a decision. Neither can do the other's job, and neither is defined here: AMP is the thing in the middle that makes a meeting look like something an agent can join.
The consequence worth caring about: your agent stays your agent. Its model, workspace, instructions, skills, tools, session and memory are its own and none of them change. AMP adds the one thing it does not have — a room to be in.
An agent, to AMP, is a command whose stdin and stdout speak ACP. That is the entire contract, which means where the agent runs is not AMP's business:
| the agent runs | how AMP reaches it |
|---|---|
| on this machine | claude-agent-acp |
| in a container | docker exec -i <worker> claude-agent-acp |
| on another host | ssh worker claude-agent-acp |
| in a cluster | kubectl exec -i <pod> -- claude-agent-acp |
| in your own cloud | any command that bridges stdio to it |
All of those are one row in agents.config.json. No AMP code changes.
One honest caveat, being fixed: the reverse direction is not yet portable. AMP currently hands the agent its meeting tools as a local command to spawn plus a loopback URL, which a remote agent can do nothing with. Making MCP an HTTP transport with a reachable URL is what finishes this claim, and it is the next substantive change. Until then, remote agents get ACP but not the meeting tools.
npm install
npm run dev # http://127.0.0.1:4321Then, in the browser: pick Fake ACP agent (built in), create the meeting, click Launch agent, and talk. Nothing else needs installing — the fake agent ships in this app and speaks real ACP.
Try, in this order:
| Say | What happens |
|---|---|
I used Codex yesterday. |
ignored — mentioned, not addressed |
We should ask the cofounder later. |
ignored — a proposal about it, not to it |
Cofounder, what do you think? |
answers, streams, and speaks aloud |
Codex, inspect the webhook retries. |
reads a file without asking, then answers |
Codex, fix the webhook retries. |
asks permission first — a write waits for a human |
Cofounder, what are the options? |
answers with a question of its own |
yes |
answers again — a reply needs no name |
To see the same run without a browser:
npm run smoke # drives the whole slice against the fake agentRun from the repository root:
| Command | What it does |
|---|---|
npm install |
install dependencies (this app installs independently) |
npm run dev |
start with reload on 127.0.0.1:4321 |
npm run build |
compile TypeScript to dist/ |
npm start |
run the compiled build |
npm test |
the full suite (223 tests, spawns real child processes) |
npm run lint |
eslint |
npm run typecheck |
tsc --noEmit over src, tests and scripts |
npm run format |
prettier |
npm run db:init |
create the SQLite database and print its tables |
npm run seed |
insert a demo meeting with participants, transcript and a memory |
npm run smoke |
end-to-end run against the fake ACP agent, no browser |
Configuration is environment variables — see .env.example, which documents
every one and why its default is what it is.
src/
domain.ts the vocabulary every layer shares
config.ts environment configuration
acp/
client.ts ONE ACP client: spawn, initialize, session, prompt,
stream, permissions, cancel, crash, shutdown
registry.ts the configurable agent registry + executable checks
events.ts session/update → normalized events
sanitize.ts ANSI/control stripping, bounded diagnostic log
gateway/
gateway.ts the Meeting Gateway — the only place meeting and agent meet
attention.ts the deterministic attention engine
prompt.ts prompt construction, SPEAK:/CHAT: parsing, speech safety
context.ts topic and rolling summary (extractive, not generated)
providers/
mock.ts the working v0 provider: a simulated room
external.ts the seam for Recall.ai / Meeting BaaS / Vexa / Meet / Zoom
queue.ts single-consumer async queue behind `events()`
mcp/
server.ts the meeting MCP server (7 tools)
bridge.ts its capability-scoped client back into the gateway
store/ SQLite schema and repository
fake-agent/ a real ACP server with a scripted model behind it
server/ Fastify app, realtime hub, composition root
public/ the meeting simulator UI (no build step)
A meeting is a continuous stream of speech and almost none of it is for the agent. Forwarding every fragment would burn a model turn per sentence, and an agent that answers side conversation is one nobody invites back. So the default is silence, and the bar for breaking it is deterministic — no model decides whether a model gets to speak.
Four things earn a turn:
- a human marks an utterance as addressed (the checkbox in the UI);
- a chat message directed at it (
@cofounder …); - a wake name used vocatively and followed by a question or instruction;
- anything at all, for 30 seconds after the agent ends a turn on a question — because somebody answering says "yes", not "Cofounder, yes".
Rule 3 does the work, because a wake name is also an ordinary noun in a meeting about coding agents. The grammar it exploits is that English marks direct address positionally: a vocative sits at the edge of its clause and is not preceded by a determiner or a verb.
| Triggers | Stays silent |
|---|---|
Cofounder, what do you think? |
I used Codex yesterday. |
Hey Codex, inspect the webhook retries. |
Claude helped write this. |
Claude: summarize our decision. |
We should ask the cofounder later. |
So what do you think, cofounder? |
Our cofounder is out this week. |
Codex can you check the retries? |
Codex and Claude are both ACP agents. |
yes — right after it asked you something |
Cofounder, nice to have you here. (addressed, nothing asked) |
Rule 4 is deliberately narrow, because the costs are asymmetric: a missed follow-up costs one repetition, a false one costs an interruption. It opens only after a turn that ends on a question — a question hands the turn back, a statement does not — and it is consumed by the first thing anyone says, so the agent cannot drift into the conversation carrying on around it.
A caption stream does not punctuate. cofounder what do you think arrives with
no comma, no question mark and no capitals, and the first version of these
rules — which required the comma — lost six of eight genuine addresses
against that input. It did not answer them wrongly; it went deaf, which in a
meeting is just as useless.
So punctuation is evidence, never a precondition. When the comma is missing the same decision is made from syntax:
- the predicate guard — what follows a vocative is an imperative or a
question, never a predicate.
claude helped write thishas the name as the subject of a finite verb and stays silent;claude write the migration noteis an instruction and does not; - the addressee is already named — a leading vocative means the clause need
not say "you" again, so
codex what's the retry budgetis heard; - fillers are stripped first, because speech opens with them constantly
(
um so cofounder …).
Transcribers also break a name apart and punctuate a pause as a full stop, so
Co founder, co-founder and Cofounder. are all matched as the one name —
each of those was observed on a live call, and each had made the agent silently
deaf.
One deliberate limit: with no comma, a trailing vocative is recognized only
after an opinion verb (what do you think cofounder). A name at the end of a
clause is usually its object — did you try codex, remind me to ping codex —
and admitting those cost three false positives when measured.
test/attention.test.ts holds 38 cases, weighted toward false positives.
test/asr.test.ts re-asserts the rules as ASR would emit them, deriving the
spoken form mechanically so the two cannot drift.
Synthesized server-side behind a synthesizeSpeech seam: a neural voice
(ElevenLabs) when a key is present, macOS say when it is not. The fallback
matters more than it looks — the repo has to run from a clone with no
credentials, so a missing key is a downgrade, never an error.
Sentences are released as the agent writes them, not when the turn ends. The room hears the first sentence while the third is still being generated, which is the difference between roughly two seconds to first audio and roughly ten. A sentence is held back until it is actually finished, because half a clause sounds worse than a pause.
The agent is asked to split its answer with SPEAK: and CHAT:, and
planSpeech enforces the split whether or not it complied:
- a
SPEAK:section is spoken — sanitized, with code removed and URLs turned into "a link in the chat", capped and truncated at a sentence boundary; - a short plain response is spoken as-is;
- anything containing code, resembling tool output, or over ~110 words is not spoken — it goes to meeting chat instead;
- tool activity is a status update, never speech.
Stopping speech and cancelling work are separate controls, deliberately.
"Stop talking" is a local action that never reaches the agent; "stop working"
is an ACP session/cancel that never touches the voice. A new direct question
while the agent is speaking is a barge-in: the voice stops, the work does not.
The registry (src/acp/registry.ts, overridable via agents.config.json — see
agents.config.example.json) ships:
| id | command | status |
|---|---|---|
fake |
the built-in fake ACP agent | works out of the box, no install |
codex |
npx -y @agentclientprotocol/codex-acp |
package verified on npm (1.6.2 at time of writing) |
claude |
claude-agent-acp |
bin published by @agentclientprotocol/claude-agent-acp (0.70.0) |
hermes |
hermes acp |
unverified — set your own command if it differs |
openclaw |
openclaw acp |
unverified — set your own command if it differs |
generic |
whatever you pass | disabled unless AMP_ALLOW_GENERIC=true |
Adapter package and binary names move. Check before trusting a row: the UI
has a "Check this agent is installed" button, and POST /api/agents/:id/check
resolves the executable on PATH without running it.
- Fake — nothing. It is spawned from this app's own
dist/(or throughtsxin dev). - Codex —
npm i -g @agentclientprotocol/codex-acp, or leave the defaultnpx -y …and let npx fetch it. It authenticates through Codex's own credentials; the client prefers thechat-gptauth method when several are advertised, because the first advertised method is not always the working one. - Claude Code —
npm i -g @agentclientprotocol/claude-agent-acpgives you theclaude-agent-acpbin.@zed-industries/claude-code-acpis an alternative that installsclaude-code-acpinstead; set whichever you have inagents.config.json. - Hermes — placeholder:
hermes acp. Verify against your build and override if it differs. - OpenClaw — placeholder:
openclaw acp. Same caveat.
Any ACP-speaking executable works: put it in agents.config.json and it appears
in the picker. Arguments are always passed as a vector; there is no code path in
this app that builds a shell command string.
When an ACP session is created, the gateway passes the meeting MCP server in
session/new's mcpServers, so a compliant agent gets these tools
automatically:
| Tool | Does |
|---|---|
meeting_get_active |
title, status, topic, rolling summary |
meeting_get_participants |
who is in the room |
meeting_get_recent_transcript |
attributed entries, oldest first |
meeting_send_chat |
post to meeting chat as the agent |
meeting_speak |
say something out loud (unspeakable text → chat) |
meeting_remember |
store a decision/action item/fact/note with source |
meeting_list_memories |
read what has been remembered |
The MCP server is spawned by the agent, so it reaches meeting state over loopback HTTP with a per-meeting capability handed to it in its environment. That capability is minted per connection, held in memory only, and never written to disk. State-changing tools validate that the meeting is live and that the caller holds that meeting's capability.
If your adapter ignores client-provided MCP servers, the ACP demo is unaffected — everything above still works. Configure it in the agent's own MCP config instead:
{
"mcpServers": {
"meeting": {
"command": "node",
"args": ["/abs/path/to/amp/dist/mcp/bin.js"],
"env": {
"AMP_MCP_BASE_URL": "http://127.0.0.1:4321",
"AMP_MCP_MEETING_ID": "mtg_...",
"AMP_MCP_TOKEN": "<capability>"
}
}
}
}Set AMP_ENABLE_MCP=false to stop passing it through ACP.
All input is schema-validated; failures return { error: { code, message } }.
GET /api/health
GET /api/agents
POST /api/agents/:agentId/check
POST /api/meetings
GET /api/meetings
GET /api/meetings/:meetingId
POST /api/meetings/:meetingId/start
POST /api/meetings/:meetingId/end
POST /api/meetings/:meetingId/participants
POST /api/meetings/:meetingId/utterances
POST /api/meetings/:meetingId/agent/connect
POST /api/meetings/:meetingId/agent/cancel
POST /api/meetings/:meetingId/agent/disconnect
POST /api/meetings/:meetingId/permissions/:requestId/respond
GET /api/meetings/:meetingId/transcript
GET /api/meetings/:meetingId/memories
POST /api/meetings/:meetingId/memories
POST /api/meetings/:meetingId/memories/:memoryId/supersede
GET /ws?meetingId=… realtime meeting feed
/api/mcp/* loopback bridge for the meeting MCP server
Two are wired in, behind one six-method MeetingProvider interface — nothing
above it knows which platform is in use.
mock (simulator) |
recall |
|
|---|---|---|
| needs | nothing | a Recall.ai API key, a public URL |
| meetings | scripted, in-process | real Google Meet · Zoom · Teams |
| use it for | development, tests, CI | actually being in a call |
The simulator is the default and always available, so the whole system runs from a clone with no credentials. Recall is offered only when fully configured.
docs/meeting-providers.md covers the rest: why a bot vendor rather than Meet's own API, the three vendor constraints that shape the design, where the latency goes, how the agent gets a voice, and the wire format as actually observed. Read it before changing the Recall provider.
SQLite through Node's built-in node:sqlite — no native compile, no database
service, nothing added to the repo's dependency surface. (The repo's convention
is Postgres + Drizzle, as in apps/chat and apps/hub; this prototype takes
the v0 escape hatch so it runs from a clone with npm install && npm run dev.
node:sqlite is flagged experimental on Node 22 and prints one warning on first
import — that is expected.)
Persisted: meetings, participants, transcript entries, chat messages, agent definitions (command and args, never credentials), ACP session associations, sanitized agent audit events, and memories with provenance.
Not persisted, by construction: authentication tokens (the MCP capability lives in memory), environment secrets, hidden chain-of-thought (thought chunks are normalized to an event carrying no text at all), and unsanitized process output.
Reads order by rowid, not by id or timestamp — several rows routinely land in
the same millisecond, and rowid is the only column that records the order they
were actually written in.
This is a local developer prototype and it behaves like one:
- binds
127.0.0.1by default, and warns loudly if you bind it wider; - an agent is only ever launched by an explicit human action, and the command, argument vector and working directory are shown before it runs;
spawnwith an argument array,shell: false, always. No command strings;- the browser cannot name an arbitrary executable — the generic agent is off unless the operator enables it on the machine;
- writes and commands are never auto-approved. They go to a human, and the timeout denies rather than allows: silence is not consent;
- reads are auto-approved, because a meeting has no approval UI and a
request that waits for a human waits for nobody — measured, that was 120
seconds of silence in a live call before the agent gave up and answered
without the file. Note what this widens: the agent's working directory is a
starting point, not a sandbox, and it runs with the permissions of the user
who started it. Set
autoApproveReads: falseon the gateway to take it back; - the agent's own sandbox and approval rules are untouched — nothing here bypasses them;
- ANSI escapes and control characters are stripped at the process boundary; transcripts, diagnostics and logs are bounded;
- child processes are terminated on shutdown (SIGTERM, then SIGKILL).
| Symptom | Cause and fix |
|---|---|
"hermes is not on PATH" when launching |
The adapter is not installed, or the command name differs in your build. Use the Check button, then override the command in agents.config.json. |
Agent launches, then immediately shows error |
The adapter exited. Look at Diagnostics in the Agent activity column — adapters report auth and quota problems on stderr. |
| The agent never answers | Check the Turn & tool activity column for ignored — …. The attention engine explains every decision it makes. Tick "Address the agent explicitly" to bypass it. |
| A turn hangs, then reports a timeout | Idle timeout (no output at all) or total timeout. Both are configurable; both cancel the turn rather than leaving the process running. |
| Permission requests never appear | Some agents auto-approve read-only tools internally, so the callback never fires. That is the agent's own policy, not this app's. |
| Nothing is spoken | No speaker page is configured, synthesis failed, or the response was suppressed — check Diagnostics for "response not spoken (…)". |
ExperimentalWarning: SQLite on startup |
Expected on Node 22; node:sqlite is flagged experimental. |
| Port already in use | AMP_PORT=… npm run dev. |
Real: the ACP client (handshake, capability negotiation, session create and load, streaming, permissions, cancellation, timeouts, crash handling, graceful shutdown), the agent registry, the attention engine, the prompt and speech pipeline including synthesis, the MCP server and its capability model, the mock provider, the Recall provider — dispatched into real Google Meet calls with a real coding agent answering out loud, persistence, the realtime feed, the UI, and the fake ACP agent.
Stubbed: ExternalMeetingProvider, the generic seam for other vendors
(Meeting BaaS, Vexa). Its ingest() and events() work; the lifecycle methods
throw NotImplementedError naming what is missing. Recall is the reference
implementation to copy — nothing above the interface knows what a platform is,
which is what keeps a second one a contained job.
Not attempted: authentication, cross-meeting memory, and speaker diarization beyond what the vendor supplies.
Honest about what is not done, in the order it bites:
- MCP is stdio-only. The gateway spawns the meeting's MCP server and hands the agent a loopback URL, which works precisely because the agent is on this machine. An agent reached over SSH, in a cluster, or in someone's cloud can still be prompted and can still speak — but it gets no meeting tools. Until this is served over HTTP, "runtime-agnostic" is true of ACP and only partly true of MCP.
- A wake name split across two transcript events is missed. The in-utterance
fixes handle
Co founderinside one event; a name broken across two needs a bounded same-speaker buffer that does not exist yet. - Attention is rules, not judgement. Deterministic is the right default and the reason it is cheap, but a model asked "was that meant for me?" would catch what grammar cannot. The decision and its reason are recorded for every utterance, so this can be settled on evidence rather than taste.
- One meeting, one agent. Nothing in the design forbids several; nothing implements it either.
Consent is a product requirement, not a technical one: a bot that joins a call is a recording device, and whatever the platform and the people in the room require has to happen first.