aep-mcp: the MCP server

The MCP server exposing AEP's LLM-fired emit path via two tools: emit_event and ask_human.

The code this page cites lives in the reference repository, agenteventprotocol/reference; file paths below are relative to that repository's source tree.

impl/mcp/server.js is the MCP server exposing AEP's LLM-fired emit path: a model calling a tool, rather than a runtime hook firing, produces the event. (The package is named aep-mcp; the folder is impl/mcp/. Folders under impl/ don't carry the aep- prefix, matching relay/, cli/, and impl/README.md's listing.) It speaks MCP over stdio: newline-delimited JSON-RPC 2.0, tools capability only, dependency-free (impl/mcp/server.js:9-10, 224-263), exposing exactly two tools, declared in TOOLS (impl/mcp/server.js:125-169):

Tool What
emit_event Emit an AEP event: type, optional data/subject/severity/session/run. Rejects malformed types and refuses control.* (toolEmitEvent, impl/mcp/server.js:173-185)
ask_human attention.requested → blocks on the control round-trip → returns the answer or reports a timeout (toolAskHuman, impl/mcp/server.js:187-222)

Identical envelopes by construction

AEP-0001 §9 requires exactly this: the LLM-fired and programmatic emit paths must produce structurally identical envelopes, so a consumer can't tell, and shouldn't need to, which path an event came from.

aep-mcp doesn't reimplement emission to satisfy that; it reuses the same emit() function shape as the adapters: per-session JSONL log under ~/.aep/logs/<agent>/, WebSocket to the relay with the same hello/outbox-until-ready pattern, and the same (epoch, seq) per-session counters seeded from a state file (impl/mcp/server.js:32-101, essentially the same code as impl/adapter-claude-code/adapter.js:49-107). Identity is structural: it's the same code path, not a parallel one kept in sync by hand.

The emit_event tool description itself carries the other half of §9's guidance to the model: don't duplicate events your runtime adapter already emits programmatically (impl/mcp/server.js:128-132). The tool exists for milestones a fleet observer would act on that nothing else is already emitting, not as a second path to the same event.

ask_human

toolAskHuman builds the same attention.requested payload shape the adapters' PermissionRequest path uses (kind, prompt, optional options, respond_via: ['control']), gates the prompt through the same redact-or-synthesize capture pipeline (AEP-0003 §9.2), and registers a pendingAttention resolver exactly like the adapters' control loop (impl/mcp/server.js:187-222, compare impl/adapter-claude-code/adapter.js:435-483). The one structural difference from the adapter case: there's no hook to hold open, so the MCP tools/call response itself is deferred. toolAskHuman returns a Promise that resolves only when onRespondCommand (impl/mcp/server.js:110-120) fires the pending resolver, or the ASK_TIMEOUT timer does.

sequenceDiagram
    participant M as Model (tools/call)
    participant S as aep-mcp server
    participant R as Relay
    participant Op as Operator (control sender)

    M->>S: tools/call ask_human<br/>{prompt, options?}
    S->>R: attention.requested<br/>(kind, prompt,<br/>respond_via:[control])
    S->>S: arm ASK_TIMEOUT_MS timer,<br/>register pendingAttention<br/>resolver
    alt operator answers in time
        Op->>R: control.attention.respond<br/>{answer}
        R->>S: control.attention.respond<br/>(routed to this<br/>connection)
        S->>R: control.accepted<br/>(cause = command id)
        S->>R: attention.answered<br/>+ attention.resolved
        S-->>M: tools/call result<br/>{answer, request_id}
    else timeout elapses
        S->>R: attention.timeout
        S-->>M: tools/call result<br/>(isError: no human<br/>answer within timeout)
    end

Unlike the adapters, there is no fallback UI to hand control back to on timeout. ask_human just reports the timeout to the model as a tool result, since an MCP tool call has nowhere else to defer to.

See also

On this page