Adding a Framework Host
This guide explains how to add support for a new agent framework (e.g., AutoGen, Semantic Kernel, OpenAI Agents SDK).
Worker-side details — the
Participantlifecycle,ctx.actionsAPI, handler dispatch, andfromBootstrap()semantics — are canonically documented in the SDK guides (Python, TypeScript). This guide only covers the macp-playground wiring (adapter, catalog entry, manifest, tests).
Step 1: Create a Host Adapter
Create src/hosting/adapters/<framework>-host-adapter.ts. The shape below mirrors
src/hosting/adapters/langgraph-host-adapter.ts, the simplest Python adapter:
import { AgentHostAdapter, PrepareLaunchInput, PreparedLaunch } from '../contracts/host-adapter.types';
import { AgentFramework, AgentManifest, ManifestValidationResult } from '../contracts/manifest.types';
import { buildAgentEnv } from './agent-env';
const DEFAULT_STARTUP_TIMEOUT_MS = 30000;
export class MyFrameworkHostAdapter implements AgentHostAdapter {
readonly framework: AgentFramework = 'myframework'; // add to the AgentFramework union first (Step 2)
validateManifest(manifest: AgentManifest): ManifestValidationResult {
const errors: string[] = [];
if (manifest.framework !== 'myframework') {
errors.push(`expected framework "myframework", got "${manifest.framework}"`);
}
if (!manifest.entrypoint?.value) {
errors.push('entrypoint.value is required');
}
const entrypointType = manifest.entrypoint?.type;
if (entrypointType && entrypointType !== 'python_module' && entrypointType !== 'python_file') {
errors.push(`myframework entrypoint type must be python_module or python_file, got "${entrypointType}"`);
}
if (manifest.frameworkConfig && typeof manifest.frameworkConfig.myRequiredField !== 'string') {
errors.push('frameworkConfig.myRequiredField is required and must be a string');
}
return { valid: errors.length === 0, errors };
}
prepareLaunch(input: PrepareLaunchInput): PreparedLaunch {
const { manifest, bootstrap } = input;
// The provider has already merged EXAMPLE_AGENT_PYTHON_PATH into manifest.host.python
// (unless the manifest pins its own), so 'python3' is only a last resort.
const pythonCmd = manifest.host?.python ?? 'python3';
const entrypoint = manifest.entrypoint.value;
const args =
manifest.entrypoint.type === 'python_module'
? ['-m', entrypoint, ...(manifest.host?.args ?? [])]
: [entrypoint, ...(manifest.host?.args ?? [])];
return {
command: pythonCmd,
args,
env: {
...(process.env as Record<string, string>),
...(manifest.host?.env ?? {}),
PYTHONUNBUFFERED: '1',
// MACP_PARTICIPANT_ID, MACP_RUN_ID, MACP_SESSION_ID, MACP_RUNTIME_ADDRESS,
// MACP_RUNTIME_TOKEN, TLS + cancel-callback vars — all derived from the flat
// BootstrapPayload (src/hosting/contracts/bootstrap.types.ts).
...buildAgentEnv(bootstrap, 'myframework'),
EXAMPLE_AGENT_ENTRYPOINT: entrypoint
},
cwd: manifest.host?.cwd ?? process.cwd(),
startupTimeoutMs: manifest.host?.startupTimeoutMs ?? DEFAULT_STARTUP_TIMEOUT_MS
};
}
}
BootstrapPayload is flat and snake_case (bootstrap.participant_id,
bootstrap.session_id, bootstrap.metadata?.run_id, …) — there is no nested
participant / run object. Prefer buildAgentEnv() over reading fields
yourself. buildAgentEnv sets MACP_BOOTSTRAP_FILE: ''; LaunchSupervisor
overwrites it with the real path at spawn time, so do not set it in the adapter.
Field reference: worker-bootstrap-contract.md.
Step 2: Update the Framework Type
Add your framework to the union in src/hosting/contracts/manifest.types.ts:
export type AgentFramework = 'langgraph' | 'langchain' | 'crewai' | 'custom' | 'myframework';
Also add it to ExampleAgentFramework in src/contracts/example-agents.ts, which
types the catalog entry in Step 6.
Step 3: Register the Adapter
In src/hosting/host-adapter-registry.ts, add:
import { MyFrameworkHostAdapter } from './adapters/myframework-host-adapter';
// In constructor:
this.register(new MyFrameworkHostAdapter());
Step 4: Create a Worker Package
Create agents/myframework_worker/ with:
__init__.pymain.py— entry point that uses the SDK<framework_specific>.py— framework setup (graph, chain, crew, etc.)mappers.py— input/output mappers
Example main.py using the upstream macp_sdk Participant abstraction (the
local macp_worker_sdk was removed in April 2026; Python workers now depend
on macp-sdk-python
from PyPI directly — pinned in agents/requirements.txt, imported as macp_sdk):
#!/usr/bin/env python3
import json, logging, os
from macp_sdk.agent import from_bootstrap
from my_framework import build_model
from mappers import map_kickoff_to_inputs
logger = logging.getLogger("macp.agent")
def _session_context() -> dict:
path = os.environ.get("MACP_BOOTSTRAP_FILE", "")
if not path:
return {}
with open(path) as f:
data = json.load(f)
return (data.get("metadata") or {}).get("session_context") or {}
def main() -> int:
participant = from_bootstrap()
model = build_model()
session_context = _session_context()
def handle_proposal(message, ctx):
output = model.invoke(map_kickoff_to_inputs(session_context))
ctx.actions.evaluate(
message.proposal_id or "",
output.get("recommendation", "REVIEW"),
confidence=output.get("confidence", 0.5),
reason=output.get("reason", "model evaluation"),
)
logger.info("evaluation sent proposalId=%s", message.proposal_id)
participant.stop()
participant.on("Proposal", handle_proposal)
participant.run()
return 0
if __name__ == "__main__":
raise SystemExit(main())
See the SDK agent-framework guides linked at the top of this doc for the
full Participant / ctx.actions surface. The macp-playground itself
does not own that contract.
Emitting ambient Signal / Progress envelopes (optional)
For diagnostics or session-level metadata, a worker can emit ambient
envelopes (mode="", session_id=""). The JWT minted for every spawn
includes "" in allowed_modes for this purpose — see
direct-agent-auth.md § Ambient envelopes.
Use the SDK's ambient helpers rather than building envelopes yourself:
MacpClient.send_signal() / send_progress() in
macp-sdk-python
and client.sendSignal() / sendProgress() in
macp-sdk-typescript.
They enforce the RFC-MACP-0001 shape rules (empty envelope session_id/mode
for Signal; Progress either fully ambient or fully session-scoped) that a
hand-built envelope gets wrong silently. The shipped Python workers' emit_signal
/ emit_progress helpers predate that guidance and still call
macp_sdk.envelope.build_envelope + ctx.actions.send_envelope directly — do
not copy them into a new worker.
Step 5: Create a Manifest
Create agents/manifests/my-agent.json. Manifests are required — loadManifest() throws INVALID_CONFIG at service startup if the file is missing or unparseable.
{
"id": "my-agent",
"name": "My Agent",
"framework": "myframework",
"version": "1.0.0",
"entrypoint": {
"type": "python_file",
"value": "agents/myframework_worker/main.py"
},
"host": {
"cwd": ".",
"env": { "PYTHONUNBUFFERED": "1" },
"startupTimeoutMs": 30000
},
"frameworkConfig": {
"myRequiredField": "value"
}
}
Do not set host.python or host.node. A manifest that names its own interpreter
silently beats the deployment-wide EXAMPLE_AGENT_PYTHON_PATH /
EXAMPLE_AGENT_NODE_PATH, and src/hosting/shipped-manifests.spec.ts fails CI for any
manifest in agents/manifests/ that does.
Step 6: Register in the Agent Catalog
Add an entry to EXAMPLE_AGENT_DEFINITIONS in src/example-agents/example-agent-catalog.service.ts.
Step 7: Add Tests
- Unit test for the adapter in
src/hosting/adapters/adapters.spec.ts src/hosting/shipped-manifests.spec.tspicks up the new manifest automatically (it must not pin an interpreter — see Step 5)- Mapper unit tests in
agents/tests/(run withpytest agents/tests), and keepruff check agents/clean - Add the agent to an existing scenario or create a new one
- Run
npm test,npm run test:e2e, andnpm run test:integration
Key Rules
- Never let framework code leak into controllers or compiler — all framework logic stays in adapters and worker packages
- Outbound envelopes go through the SDK — mode messages via the handler context's
ctx.actions.{evaluate,vote,commit,...}(both SDKs), ambient Signal/Progress via the client'ssend_signal/sendSignalhelpers. Do NOT construct envelopes by hand; do NOT POST to any control-plane/runs/:id/messagesroute (deleted). - Workers never call the control plane — the control plane is an
observer-only projection. All read and write traffic flows through the
per-agent gRPC channel to the runtime, authenticated with
bootstrap.auth_token. - Validate manifests before spawn — bad config should fail fast with clear errors
- Framework workers must gracefully fall back when framework libraries aren't installed
- Respect direct-agent-auth invariants — identity flows through
bootstrap.auth_token(mirrored to theMACP_RUNTIME_TOKENenv var bysrc/hosting/adapters/agent-env.ts); the SDK enforcesexpectedSendermatching the authenticated sender (RFC-MACP-0004 §4). Seedocs/direct-agent-auth.mdfor the end-to-end flow.