Onboarding an agent
How to add a new MACP-compliant agent to a deployment. One page, five steps.
How to add a new MACP-compliant agent to a deployment. One page, five steps.
Under the direct-agent-auth architecture:
- Every agent authenticates to the runtime directly via a Bearer credential — either a static token or a short-lived JWT (see Step 2).
- The control-plane never emits envelopes on behalf of agents — it is a scenario-agnostic observer.
- The initiator agent of a session calls
Send(SessionStart)itself; non-initiator participants open their ownStreamSessionto receive events and emit their own envelopes.
This satisfies RFC-MACP-0004 §3 (sender MUST be derived from authenticated identity) and RFC-MACP-0001 §5.3 (no MACP bypass). The reference deployment (macp-playground) documents its own side of this — bootstrap files, JWT minting, policy registration — in docs/direct-agent-auth.md; the SDKs document the agent-side patterns (initiator/non-initiator code, the expected_sender guardrail, session.cancel()) in macp-sdk-python/docs/guides/direct-agent-auth.md and macp-sdk-typescript/docs/guides/authentication.md.
This page shows two ways to get a Bearer credential to your agent — pick whichever matches your scenario-producing tier:
- Static bearer tokens (Steps 2A/3A) — works with any runtime deployment, no extra services required.
- On-demand JWT minting (Steps 2B/3B) — how the reference
macp-playgrounddeployment does it today; nothing to register per agent.
Prerequisites
- A running MACP runtime with its gRPC endpoint reachable from your agent process (e.g.,
runtime.internal:50051). - Static bearer path: admin access to the runtime's environment (
MACP_AUTH_TOKENS_JSON/MACP_AUTH_TOKENS_FILE). - JWT path: admin access to the runtime's JWT resolver config (
MACP_AUTH_ISSUER,MACP_AUTH_AUDIENCE,MACP_AUTH_JWKS_URL) and, if usingmacp-playground, to itsMACP_AUTH_SERVICE_URLsetting — see that repo's deployment checklist. - Either
macp-sdk-python >= 0.2.0(PyPI) ormacp-sdk-typescript >= 0.2.0(npm) available to your agent runtime.
Step 1 — Decide the agent's sender id
The sender is the plain-string identity the runtime binds to this agent (RFC-MACP-0001 §6). Bare names are fine:
risk-agent
fraud-agent
my-new-agentThe agent://… prefix is a convention used by some integration tests, not a protocol requirement.
Rules:
- Must be non-empty.
- Must be unique across all agents that can talk to the same runtime identity registry.
- Must match the
senderthe runtime resolves for this agent's credential (step 2).
Step 2 — Configure the runtime's auth resolver
This is a one-time deployment setting, not a per-agent step — the runtime can run either or both resolvers at once (JWT-shaped tokens route to the JWT resolver, opaque tokens to the static one).
Option A — Static bearer tokens
Generate a strong random token:
openssl rand -hex 32Add an entry to the runtime's MACP_AUTH_TOKENS_JSON. The runtime loads this map once at boot (crates/macp-auth/src/security.rs, AuthConfig::from_env):
{
"tokens": [
{ "token": "<existing-agents>", "sender": "risk-agent", "can_start_sessions": true },
{ "token": "<existing-agents>", "sender": "fraud-agent", "can_start_sessions": true },
// add:
{
"token": "<your-new-token>",
"sender": "my-new-agent",
"can_start_sessions": true,
"allowed_modes": ["macp.mode.decision.v1"],
"max_open_sessions": 10
}
]
}Capability guidance:
| Flag | Set it when… |
|---|---|
can_start_sessions: true | The agent may be a session initiator for at least one scenario. |
allowed_modes: [...] | You want to restrict which modes the agent may send in. Empty/absent = unrestricted. |
max_open_sessions: N | You want per-agent concurrency caps. |
can_manage_mode_registry: true | The agent manages registered modes/policies (rare; usually false). |
Redeploy the runtime (or wait for hot-reload, if your deployment supports it). Continue to Step 3, Option A.
Option B — JWT verification (the macp-playground reference deployment)
Point the runtime at a JWKS source instead of a static map — no per-agent entry needed here, the runtime derives sender from each JWT's sub claim:
export MACP_AUTH_ISSUER=<issuer>
export MACP_AUTH_AUDIENCE=<audience> # default: macp-runtime
export MACP_AUTH_JWKS_URL=<auth-service>/.well-known/jwks.jsonThe default algorithm allowlist is RS256/ES256; HS256 requires an explicit MACP_AUTH_JWT_ALGS=HS256 opt-in. Continue to Step 3, Option B.
Step 3 — Get the Bearer credential to your agent process
Option A — Static bearer (your own scenario-producing tier)
Inject the token you generated in Step 2A into your agent's bootstrap however your own tier does configuration — an env var, a secrets manager entry, whatever fits. The only requirement is that the agent's bootstrap ends up with the correct Bearer token in its runtime-auth field (e.g. auth_token).
Option B — macp-playground (automatic, nothing to register)
The playground mints a short-lived RS256 JWT per agent spawn — AuthTokenMinterService calls POST /tokens on the auth-service configured via MACP_AUTH_SERVICE_URL, scoped from the agent's role (can_start_sessions, allowed_modes) with optional per-sender overrides via MACP_AUTH_SCOPES_JSON, and bakes the result into the agent's bootstrap file. There is no static token map to maintain and no per-agent entry to add. See the "AUTH-2" section of docs/direct-agent-auth.md for the minting flow, caching, and TTL constraints.
Step 4 — Register the agent in the scenario catalog (macp-playground only)
Skip this step if you're wiring an agent into your own scenario-producing tier — it's specific to the macp-playground reference implementation, which needs two files:
- Add an entry to
src/example-agents/example-agent-catalog.service.ts:
{
agentRef: 'my-new-agent',
name: 'My New Agent',
role: 'evaluator',
description: 'What this agent evaluates.',
framework: 'custom', // or 'langgraph' | 'langchain' | 'crewai'
supportedScenarioRefs: ['fraud/high-value-new-device@1.0.0'],
}- Create a matching launch manifest at
agents/manifests/my-new-agent.json:
{
"id": "my-new-agent",
"name": "My New Agent",
"framework": "custom",
"version": "1.0.0",
"entrypoint": { "type": "python_file", "value": "agents/my_new_agent/main.py" },
"host": { "cwd": ".", "env": {}, "startupTimeoutMs": 30000 },
"macp": { "role": "evaluator", "supportedMessageTypes": ["Evaluation"], "capabilities": [] }
}This AgentManifest (macp-playground's own src/hosting/contracts/manifest.types.ts shape — framework, entrypoint, host process config) is a different document from the protocol's AgentManifest: the discovery/capability document defined in RFC-MACP-0005 §3 (Manifest Structure) and schemas/json/macp-agent-manifest.schema.json (see docs/discovery.md/docs/agent-manifest-schema.md). The two share a name and nothing else — neither field set overlaps the other.
Then add the agent to the scenario's participants list in its YAML.
Step 5 — Pick an SDK and wire the agent loop
Both SDKs ship a high-level agent/Participant framework that reads the bootstrap file for you (auth, transport, projection, and cancel-callback wiring included) and dispatches to handlers you register — you no longer hand-build a MacpClient or drive a raw stream loop yourself. See each SDK's own Agent Framework guide for the full API: macp-sdk-python/docs/guides/agent-framework.md, macp-sdk-typescript/docs/guides/agent-framework.md.
Python
from macp_sdk.agent import from_bootstrap
participant = from_bootstrap() # reads $MACP_BOOTSTRAP_FILE
def on_proposal(msg, ctx):
payload = msg.payload # mode-specific proto message (decoded)
ctx.actions.evaluate(payload.proposal_id, "APPROVE", confidence=0.9)
def on_voting(phase, ctx):
ctx.log("entering voting phase")
def on_done(result):
print("terminal:", result.state, result.commitment)
# on()/on_phase_change()/on_terminal() are plain fluent methods, not
# decorator factories -- each takes the handler as a direct argument.
participant.on("Proposal", on_proposal)
participant.on_phase_change("Voting", on_voting)
participant.on_terminal(on_done)
participant.run() # blocks until a terminal event fires or stop() is calledNon-initiator agents work identically — from_bootstrap() only emits SessionStart (and a kickoff, if present) when the bootstrap document has an initiator block; otherwise the participant just subscribes and reacts to handlers.
TypeScript
import { agent } from 'macp-sdk-typescript';
const participant = agent.fromBootstrap(); // reads MACP_BOOTSTRAP_FILE
participant
.on('Proposal', async (msg, ctx) => {
ctx.log('Received proposal', { option: msg.payload.option });
await ctx.actions.evaluate({
proposalId: msg.payload.proposalId,
recommendation: 'approve',
confidence: 0.9,
reason: 'Meets criteria',
});
})
.on('Evaluation', async (msg, ctx) => {
await ctx.actions.vote({
proposalId: msg.payload.proposalId,
vote: 'approve',
reason: 'Evaluation looks good',
});
})
.onTerminal((result) => {
console.log('Session resolved:', result.state);
});
await participant.run();Same non-initiator behavior as Python: agent.fromBootstrap() only drives SessionStart/kickoff when the bootstrap document's initiator block is present.
Cancellation (Option A — RFC-pure default)
Both SDKs auto-bind a local HTTP POST <cancel_callback.path> listener for you on the bootstrap path — you don't need to hand-roll one. They bind at different moments, which matters only if your integration never starts the participant: the Python SDK binds while building the participant from the bootstrap, so the listener is live as soon as you hold the object, whereas the TypeScript SDK stores the config at construction and starts the listener when the participant begins running. A TypeScript integration that constructs a participant and never runs it therefore has no listener auto-bound (it can still attach one itself), and the troubleshooting row below is what that looks like from the runtime's side. (Either SDK's binding timing is a library choice, not a protocol requirement — see sdk-parity.md's ## MAY Implement.) The control-plane's UI-triggered cancel calls that listener; the SDK responds by calling session.cancel(reason) on the runtime with its own identity. Runtime enforces RFC-MACP-0001 §7.3 (Termination) — only the initiator (or a policy-delegated role) may cancel. See the SDK guides linked above if you need to override the default cancel behavior.
Suspension and Resume
Neither SDK's agent framework exposes an on_suspend/on_resume handler — there's nothing to
register a reactive callback for, the way on_phase_change/on_terminal work. Either SDK does
let you trigger suspension directly, same authority model as session.cancel(reason):
session.suspend(reason) / session.resume(reason). While a session is SUSPENDED the
runtime rejects Mode messages (it is not OPEN); your agent's send/evaluate/vote calls during
that window fail the same way a late message to a terminal session would, and normal handling
resumes once the session is back to OPEN. See
docs/lifecycle.md for the full model.
The one piece an initiator agent configures is max_suspend_ms — an optional per-session cap
on cumulative suspended duration (0/absent selects the runtime default). Both SDKs read it from
the bootstrap document's initiator.session_start.max_suspend_ms field identically; pass it
yourself — participant.start_session(max_suspend_ms=...) (Python) or
session.start({ maxSuspendMs: ... }) (TypeScript) — if you're not using a bootstrap-driven
initiator. See RFC-MACP-0001 §7.5.
Verify
- Launch a scenario that includes your agent.
- Watch the runtime logs — your agent's envelopes should show
sender=<your-agent-id>. - Watch the control-plane's run event feed — no
UNAUTHENTICATEDerrors. - If the agent is the initiator: runtime logs show
SessionStart accepted, initiator_sender=<your-agent-id>.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
Agent logs UNAUTHENTICATED on first send | Runtime doesn't recognize the credential | Static bearer: check Step 2A — token and sender in MACP_AUTH_TOKENS_JSON must match, then redeploy. JWT: check Step 2B — issuer/audience/JWKS config on the runtime. |
| Agent's bootstrap is missing its Bearer credential | Credential never reached the agent | Static bearer: check Step 3A. macp-playground: check its auth_mint_failure logs and that MACP_AUTH_SERVICE_URL is reachable (Step 3B). |
Initiator's SessionStart is rejected with Forbidden | can_start_sessions: false on the identity | Static bearer: flip it to true in MACP_AUTH_TOKENS_JSON. JWT: check the minted token's allowed_modes/can_start_sessions scopes. |
Agent sends envelopes but they're rejected with sender does not match identity | Sender string mismatch | The string in bootstrap.participant_id, the envelope's sender field, and the runtime-resolved identity sender must all be identical byte-for-byte. Check for stray agent:// prefixes. |
| Cancel from UI doesn't take effect | Missing or unreachable cancel-callback listener | Verify the SDK's auto-bound listener is reachable from the control-plane and bootstrap.cancel_callback is populated. |
See also
macp-playground/docs/direct-agent-auth.md— reference deployment's bootstrap production, JWT minting, and policy registrationmacp-sdk-python/docs/guides/direct-agent-auth.mdandmacp-sdk-typescript/docs/guides/authentication.md— agent-side patternsschemas/json/macp-run-descriptor.schema.json— control-planePOST /runscontractschemas/json/macp-agent-bootstrap.schema.json— agent bootstrap contractschemas/json/macp-session-metadata.schema.json— session metadata runtime returns- RFC-MACP-0004 §3 (Authentication) + §4 (Authorization) + §11 (Multi-tenancy)
- RFC-MACP-0001 §7 (Session lifecycle) + §7.3 (Termination — cancellation authority)