A role-scoped agent mesh is an architecture in which multiple AI agent personas — each with a distinct role, workspace, and restricted set of tools — operate within a shared orchestration framework. Rather than deploying a single omniscient agent that has access to every API and data source, the mesh decomposes complex workflows into specialized agents that can only invoke tools relevant to their assigned role. The platform’s agent-mesh.json configuration defines exactly this pattern: orchestrator, researcher, data-scientist, growth-marketer, and content-writer agents, each bound to specific platform MCP tool allowlists and isolated workspace directories.
The design draws from the principle of least privilege applied to AI systems. A content-writer agent has no reason to call a database migration tool; a data-scientist agent should not be able to send outbound marketing emails. By scoping each agent’s MCP tool allowlist at the configuration level, the mesh enforces these boundaries before prompts are even constructed. This is fundamentally different from relying on prompt instructions alone (“don’t use tool X”) — configuration-level restrictions cannot be bypassed by prompt injection or model hallucination.
The orchestrator agent sits at the top of the mesh and routes tasks to the appropriate specialist. It has visibility into all agents’ capabilities but does not directly execute domain-specific tools. Instead, it decomposes incoming requests into sub-tasks, dispatches them to the correct agent based on role matching, collects results, and synthesizes a final response. This hub-and-spoke topology keeps coordination logic separate from domain logic and allows new agent roles to be added by editing a JSON configuration file rather than rewriting orchestration code.
Each agent in the mesh is assigned an explicit list of MCP tools it is permitted to invoke. The allowlist is defined in the agent’s configuration block — not in its system prompt — so enforcement happens at the MCP gateway layer before any tool call reaches the backend. This prevents both accidental and adversarial tool use outside the agent’s designated scope, providing a hard security boundary rather than a soft prompt-based suggestion.
Each agent operates within its own workspace directory, which contains role-specific context files, scratch space for intermediate results, and any persistent state the agent needs across turns. Workspace isolation means that a researcher agent’s downloaded documents cannot be accidentally overwritten by the content-writer’s drafts. The orchestrator can read from any agent’s workspace to collect results but writes only to its own coordination space.
The orchestrator agent does not perform domain work directly. Its role is to classify incoming requests, select the appropriate specialist agent (or chain of agents), format sub-task prompts with the right context, and merge results. This separation means the orchestrator’s system prompt focuses on routing logic and quality control, while specialist prompts focus on domain expertise. The orchestrator can also implement retry logic, fallback routing, and result validation without polluting specialist agent prompts.
The Model Context Protocol provides a standardized JSON-RPC interface for tool invocation, making it possible for agents to call tools without knowing their implementation details. In the mesh, each agent’s MCP client is configured with a filtered view of the server’s tool catalog — it can only “see” tools on its allowlist. This filtering happens at the MCP gateway, which acts as a policy enforcement point between the agent’s LLM and the tool backends.
The entire mesh topology — agent names, roles, model assignments, system prompts, tool allowlists, and workspace paths — is declared in a single JSON configuration file (agent-mesh.json). This means adding a new agent role requires no code changes: define a new block with its role, allowlist, and workspace, and the orchestration framework picks it up on the next initialization. This declarative approach also makes the mesh auditable — a security reviewer can inspect the JSON to verify that no agent has overly broad tool access.
At startup, the mesh framework reads agent-mesh.json and instantiates each agent as a separate conversation context. Each agent gets its own Anthropic API client (or shares a client with role-specific parameters), a system prompt tailored to its role, and an MCP client configured with only the tools listed in its allowlist. The orchestrator is initialized last, after all specialist agents are ready, so it can register their capabilities in its routing table.
Workspace directories are created if they don’t exist, and any persistent state files (e.g., a researcher’s bookmark of previously analyzed sources) are loaded into the agent’s initial context. This ensures continuity across sessions without requiring the orchestrator to re-brief each agent on prior work.
When a user request arrives, the orchestrator first classifies it — is this a research task, a data analysis task, a content creation task, or a multi-step workflow that requires chaining agents? Classification can be as simple as keyword matching or as sophisticated as a dedicated LLM call with a routing prompt. Once the target agent is identified, the orchestrator constructs a sub-task prompt that includes the original request, any relevant context from the orchestrator’s workspace, and formatting instructions specific to the specialist’s expected output format.
The sub-task is dispatched by invoking the specialist agent’s conversation endpoint. The specialist processes the request using its scoped tools — for example, the data-scientist might call platform.query and platform.visualize but cannot call platform.send_email. The specialist’s response is returned to the orchestrator, which may post-process, validate, or combine it with other specialists’ outputs before returning the final result to the user.
The MCP gateway maintains a mapping from agent identity to permitted tool names. When an agent’s LLM generates a tool call, the gateway intercepts the JSON-RPC request and checks the tool name against the agent’s allowlist. If the tool is not permitted, the gateway returns an error response without forwarding the call to the tool backend. This enforcement is synchronous and happens before any side effects — the tool function is never invoked for unauthorized calls.
The gateway also logs all tool call attempts (both permitted and denied) for audit purposes. This audit trail is valuable for debugging agent behavior (“why did the growth-marketer try to call the database migration tool?”) and for tuning allowlists over time as agent roles evolve.
// agent-mesh.json — declarative agent mesh definition
{
"mesh": {
"name": "agent-mesh",
"mcpServer": "http://localhost:3100",
"defaultModel": "claude-sonnet-4-20250514"
},
"agents": [
{
"id": "orchestrator",
"role": "orchestrator",
"model": "claude-sonnet-4-20250514",
"workspace": "./workspaces/orchestrator",
"systemPrompt": "You are the task router. Classify requests and dispatch to specialist agents. Never call domain tools directly.",
"allowedTools": ["agent.dispatch", "agent.list", "workspace.read"],
"canDispatchTo": ["researcher", "data-scientist", "growth-marketer", "content-writer"]
},
{
"id": "researcher",
"role": "researcher",
"model": "claude-sonnet-4-20250514",
"workspace": "./workspaces/researcher",
"systemPrompt": "You are a research specialist. Gather, analyze, and summarize information from the platform data sources.",
"allowedTools": ["platform.search", "platform.fetch_document", "platform.summarize", "workspace.write"]
},
{
"id": "data-scientist",
"role": "data-scientist",
"model": "claude-sonnet-4-20250514",
"workspace": "./workspaces/data-scientist",
"systemPrompt": "You are a data analysis specialist. Query datasets, compute metrics, and generate visualizations.",
"allowedTools": ["platform.query", "platform.aggregate", "platform.visualize", "workspace.write"]
},
{
"id": "growth-marketer",
"role": "growth-marketer",
"model": "claude-sonnet-4-20250514",
"workspace": "./workspaces/growth-marketer",
"systemPrompt": "You are a growth marketing specialist. Draft campaigns, analyze funnels, and optimize outreach.",
"allowedTools": ["platform.crm_search", "platform.send_email", "platform.campaign_stats", "workspace.write"]
},
{
"id": "content-writer",
"role": "content-writer",
"model": "claude-sonnet-4-20250514",
"workspace": "./workspaces/content-writer",
"systemPrompt": "You are a content creation specialist. Write, edit, and format content for various channels.",
"allowedTools": ["platform.draft_post", "platform.format_markdown", "platform.check_tone", "workspace.write"]
}
]
}
import Anthropic from '@anthropic-ai/sdk';
import { readFileSync } from 'fs';
interface AgentConfig {
id: string;
role: string;
model: string;
workspace: string;
systemPrompt: string;
allowedTools: string[];
canDispatchTo?: string[];
}
interface MeshConfig {
mesh: { name: string; mcpServer: string; defaultModel: string };
agents: AgentConfig[];
}
// Load and parse the mesh configuration
const meshConfig: MeshConfig = JSON.parse(
readFileSync('./agent-mesh.json', 'utf-8')
);
// Build a lookup from agent ID to its allowed tool set
const allowlistByAgent = new Map<string, Set<string>>();
for (const agent of meshConfig.agents) {
allowlistByAgent.set(agent.id, new Set(agent.allowedTools));
}
/**
* Enforce tool access before forwarding to the MCP server.
* Returns an error result if the agent is not permitted to call the tool.
*/
function enforceAllowlist(
agentId: string,
toolName: string
): { allowed: boolean; reason?: string } {
const allowed = allowlistByAgent.get(agentId);
if (!allowed) {
return { allowed: false, reason: `Unknown agent: ${agentId}` };
}
if (!allowed.has(toolName)) {
console.warn(
`[gateway] DENIED: agent "${agentId}" attempted tool "${toolName}"`
);
return {
allowed: false,
reason: `Agent "${agentId}" (role: ${
meshConfig.agents.find((a) => a.id === agentId)?.role
}) is not permitted to call "${toolName}"`,
};
}
return { allowed: true };
}
/**
* Process a tool call from an agent, enforcing the allowlist
* before forwarding to the actual MCP server.
*/
async function processToolCall(
agentId: string,
toolName: string,
toolInput: Record<string, unknown>
): Promise<unknown> {
const check = enforceAllowlist(agentId, toolName);
if (!check.allowed) {
return { error: check.reason, code: 'TOOL_NOT_PERMITTED' };
}
// Forward to the MCP server
const response = await fetch(`${meshConfig.mesh.mcpServer}/call`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ tool: toolName, input: toolInput }),
});
return response.json();
}
/**
* Run a specialist agent with its scoped tool access.
* The orchestrator calls this to dispatch sub-tasks.
*/
async function runSpecialistAgent(
agentId: string,
taskPrompt: string
): Promise<string> {
const agentCfg = meshConfig.agents.find((a) => a.id === agentId);
if (!agentCfg) throw new Error(`Agent not found: ${agentId}`);
const client = new Anthropic();
const messages: Anthropic.MessageParam[] = [
{ role: 'user', content: taskPrompt },
];
// Agentic loop: keep running until the model stops requesting tools
let response = await client.messages.create({
model: agentCfg.model,
max_tokens: 4096,
system: agentCfg.systemPrompt,
messages,
});
while (response.stop_reason === 'tool_use') {
const toolBlock = response.content.find(
(b): b is Anthropic.ToolUseBlock => b.type === 'tool_use'
);
if (!toolBlock) break;
// Enforce allowlist and get result
const result = await processToolCall(
agentId,
toolBlock.name,
toolBlock.input as Record<string, unknown>
);
messages.push({ role: 'assistant', content: response.content });
messages.push({
role: 'user',
content: [
{
type: 'tool_result',
tool_use_id: toolBlock.id,
content: JSON.stringify(result),
},
],
});
response = await client.messages.create({
model: agentCfg.model,
max_tokens: 4096,
system: agentCfg.systemPrompt,
messages,
});
}
// Extract final text response
const textBlock = response.content.find(
(b): b is Anthropic.TextBlock => b.type === 'text'
);
return textBlock?.text ?? '';
}
On the platform, the role-scoped agent mesh powers a daily operations workflow that would be impractical with a single agent. When the system receives a high-level directive like “prepare this week’s growth report,” the orchestrator breaks it into sub-tasks: the researcher gathers market data and competitor updates using platform.search and platform.fetch_document; the data-scientist queries CRM pipeline metrics with platform.query and generates funnel visualizations with platform.visualize; the growth-marketer pulls campaign performance stats via platform.campaign_stats; and the content-writer synthesizes everything into a formatted report using platform.format_markdown.
Each agent operates in its own workspace, so the researcher’s raw data downloads don’t collide with the content-writer’s draft files. The orchestrator collects each specialist’s output, validates completeness (e.g., checking that the data-scientist returned all requested metrics), and either requests revisions from a specialist or assembles the final report. This decomposition allows each agent’s system prompt to be focused and concise — a data-scientist prompt that only needs to handle queries and charts is far more reliable than a monolithic prompt that also handles research, writing, and email sending.
The pattern also enables progressive capability expansion. When the team needed to add SEO analysis, they defined a new agent block in agent-mesh.json with access to platform.seo_audit and platform.keyword_rank, added the new agent ID to the orchestrator’s canDispatchTo list, and updated the orchestrator’s system prompt with routing rules for SEO-related requests. No existing agent configurations were modified, and no code was deployed — the mesh picked up the new agent on the next restart.
workspace.read on all workspaces, or adding “just in case” tools to an allowlist, erodes the security benefit of role scoping. Each tool on an allowlist should have a clear justification tied to the agent’s role. Audit allowlists regularly and remove tools that an agent has never called.platform.query), it will inevitably start handling tasks directly rather than dispatching them. Keep the orchestrator’s tools limited to coordination primitives like agent.dispatch and workspace.read.<researcher_output>...</researcher_output>).agent-mesh.json may reference tools that no longer exist (causing silent failures) or miss new tools that an agent should have access to. Validate allowlists against the live tool catalog at startup and warn about mismatches.