Status: active
Model Context Protocol — how AI sessions invoke
platform.*tools, how the MCP server dispatches them, and where the tool registry lives.
- What this concept covers
platform.*tool registry- Claude Code integration
- Telemetry and monitoring
- Adding a new tool
- Tool catalog
- Related concepts
- Extending the Tool Registry
- Materials previously at
MCP (Model Context Protocol) is how external AI sessions (Claude Code, custom agents, the chat concierge) reach into Powernode to invoke platform capabilities. The platform exposes its surface as platform.* tool actions — every action a session can take, from agent execution to Docker container lifecycle, is a registered tool with a JSON Schema, permission gate, and audit trail.
This document covers the tool registry and the MCP server that fronts it. For the live action catalog (every tool, every parameter, every example), see reference/auto/mcp-tools.md — that file is auto-generated from Ai::Tools::PlatformApiToolRegistry.all_tools (static core TOOLS plus any extension-registered tools) and is the only source of truth for what's currently registered. Do not inline counts; query the catalog.
The platform exposes its capability surface through Ai::Tools::PlatformApiToolRegistry, which maps action names (platform.create_agent, platform.docker_list_containers, etc.) to tool classes that handle parameter validation, permission checks, execution, and audit logging.
flowchart TB
Session[Claude Code session<br/>or external MCP client]
Registry[Ai::Tools::PlatformApiToolRegistry]
Tools[Tool classes by domain<br/>agent, team, docker, system, ...]
Catalog[reference/auto/mcp-tools.md<br/>auto-generated]
Shared[Shared infrastructure:<br/>permissions, audit logs,<br/>SyncExecutionService,<br/>broadcast/ActionCable]
Session --> Registry
Registry --> Tools
Registry --> Catalog
Tools --> Shared
# server/app/services/ai/tools/<domain>_tool.rb
class Ai::Tools::AgentTool
ACTIONS = {
"create_agent" => :create_agent,
"list_agents" => :list_agents,
# ...
}.freeze
def action_definitions
{
"create_agent" => {
description: "Create a new AI agent with provider and model config",
parameters: { ... } # JSON Schema
},
# ...
}
end
def create_agent(params)
# Permission check
# Execute via service
# Audit log
# Return structured result
end
end# server/app/services/ai/tools/platform_api_tool_registry.rb
class Ai::Tools::PlatformApiToolRegistry
TOOLS = {
"agent" => Ai::Tools::AgentTool,
"team" => Ai::Tools::TeamTool,
"docker" => Ai::Tools::DockerTool,
"system" => Ai::Tools::SystemTool,
# ... etc
}.freeze
endTools are organized by domain. For the live catalog with full parameter schemas, see reference/auto/mcp-tools.md. Categories include:
- Discovery & context —
search_knowledge,query_learnings,discover_skills,search_memory,get_api_reference,code_semantic_search - Knowledge contribution —
create_learning,create_knowledge,extract_to_knowledge_graph,create_skill - Quality & reinforcement —
verify_learning,reinforce_learning,rate_knowledge,resolve_contradiction - Agent management —
create_agent,list_agents,execute_agent - Team management —
create_team,add_team_member,execute_team - Knowledge graph —
get_graph_node,search_knowledge_graph,reason_knowledge_graph - Memory —
write_shared_memory,read_shared_memory,consolidate_memory - RAG —
query_knowledge_base,add_document,process_document - AI autonomy & safety —
emergency_halt,kill_switch_status,create_proposal,escalate - Codebase intelligence —
code_context_tree,code_blast_radius,code_static_analysis - DevOps & CI/CD —
dispatch_to_runner,trigger_pipeline,get_pipeline_status - Docker management —
docker_list_containers,docker_deploy_stack,docker_node_promote - System extension —
system_*actions when the system extension is enabled
cd server
bundle exec rails mcp:generate_tool_catalogThis rake task introspects Ai::Tools::PlatformApiToolRegistry.all_tools, calls action_definitions on each tool class, and writes docs/reference/auto/mcp-tools.md. The catalog regenerates automatically on tool-class change and is the only source for action counts and parameter shapes.
Claude Code invokes platform.* tools directly via the streamable-HTTP MCP server registered in .claude/settings.json. The relevant entry points at http://localhost:3000/api/v1/mcp/message. No external daemon, no helper scripts — Claude Code calls the tool by name and the Rails MCP controller dispatches through the registry.
The session lifecycle:
| Phase | Behavior |
|---|---|
| Connect | Claude Code opens streamable-HTTP session; server creates McpSession row |
| Authenticate | OAuth handshake; session bound to a Powernode User and Account |
| Discovery | Claude Code lists available tools via tools/list MCP method — one-line descriptions; platform.describe_tool returns a tool's full entry on demand |
| Invocation | Claude Code calls tools/call with action name and params; server validates permission, dispatches to tool class, returns structured result |
| Reconnect | Lost connections reuse the same session if within the 10-minute grace window — agent state survives reconnects |
| Cleanup | A daily job at 3 AM cleans up expired sessions |
For configuring Claude Code itself, see getting-started/01-quickstart.md and the .claude/settings.json reference in guides/devops.md.
The integration runs in both directions. Platform agents are also Claude Code subagents: the canonical (global, seeded) agents are exported as committed, slug-keyed skeletons under .claude/agents/powernode/ that bootstrap themselves over MCP (platform_get_agent by slug, platform_get_skill_context), and platform.route_task ranks the same canonical set — with the reasons and the delegation policy applied — for both an MCP caller and the platform's own Concierge. Hand-authored Claude Code agents come back as agent_create proposals, never as direct writes. See guides/use-powernode-from-claude.md → Platform agents as Claude Code subagents.
Tool invocations emit structured audit and metric events so operators can trace which session called which action, with what parameters, and to what effect.
Every tool action writes an AuditLog row capturing the action name, invoking User/Account, parameters, and a result summary. This is the authoritative record of MCP-driven changes and feeds the governance surfaces.
State changes triggered by tool actions push to subscribed UI components over ActionCable:
ActionCable.server.broadcast(
"ai_orchestration:account:#{account.id}",
{
event: 'tool.executed',
payload: {
action: action_name,
status: status,
executed_at: Time.current.iso8601
}
}
)See concepts/chat-and-realtime.md for the channel layout.
-
Create the tool class in
server/app/services/ai/tools/:class Ai::Tools::MyDomainTool ACTIONS = { "my_action" => :my_action }.freeze def action_definitions { "my_action" => { description: "What this does", parameters: { /* JSON Schema */ } } } end def my_action(params) # validation # permission check # execution # audit log end end
-
Add action→class mapping to
PlatformApiToolRegistry::TOOLS -
Define
action_definitionswith descriptions and parameter schemas -
Regenerate the catalog:
cd server && bundle exec rails mcp:generate_tool_catalog
-
Update relevant concept docs if the tool is part of a documented capability
-
Create a learning via
platform.create_learningcategorypatterndocumenting the new tool — feeds future agent sessions
- Add a deprecation notice to the
action_definitionsdescription - Create a learning via
platform.create_learningcategorybest_practicedocumenting the replacement - Remove from concept docs after the migration period
Identity over MCP is read-only. This is a decision, not an omission.
Agents may READ users, roles, the permission catalog and the audit log. Every identity WRITE — creating or suspending a user, assigning a role, granting a permission, cleaning up the audit log — stays operator-only, through REST and the console. There is no MCP verb for any of them and adding one needs a deliberate decision, not a pull request.
The asymmetry is the point. An agent that can read the audit log can explain what happened.
An agent that can assign itself a role has closed the loop on its own authority, and that is
the one loop the platform must not close. The same reasoning keeps respond_to_approval off
the surface: the gate mints approvals to reach a person, so an agent that could answer them
would be approving its own work.
Three rules follow for anyone adding a verb in this area:
- Floor each verb on the permission its REST twin already checks. A tool must never be a way around a gate a person has to pass. Where a REST controller accepts several permissions, pick the tightest — narrower than REST is a safe difference, wider never is.
- Serialize with an ALLOW-LIST, never
.attributesand never a denylist. A column added tousersorwebhook_endpointstomorrow must not appear in a payload by default. A denylist has to be remembered; an allow-list cannot be forgotten. - Assert the absence of secrets by planting one and grepping the response. Checking that
a key is missing passes a serializer that renamed it.
Ai::Provider#credentialsreturns a DECRYPTED hash,webhook_endpoints.secret_keyand.signature_secretare PLAINTEXT columns, andaudit_logs.metadatacarries no redaction filter of its own — the read tools withhold all four, and their specs prove it with real values.
The read surface for these families is IdentityReadTool, ProviderReadTool,
ScheduleReadTool and WebhookReadTool (all core, all declared mutating: false).
One thing these verbs do NOT give you: an audit row. audit: true on a tool action is
reserved for irrevocable handouts, so an MCP read of identity, providers, schedules or
webhooks leaves no trace of who read what. Being able to tell afterwards who read a record is
therefore a property of the REST doors these verbs mirror, not of the verbs themselves — which
is why rules 1-3 above are the whole of the protection here.
class Ai::Tools::MyTool
def my_action(params)
# 1. Validate configuration
validate_params!(params)
# 2. Permission check
require_permission(current_user, "my.domain.action")
# 3. Execute via service
result = MyDomainService.new(account: account).execute(params)
# 4. Audit log
AuditLog.create!(action: "my.action", payload: params, result: result.summary)
# 5. Return structured format
{
success: true,
data: result,
metadata: { action: "my_action", executed_at: Time.current.iso8601 }
}
end
private
def validate_params!(params)
raise ArgumentError, "Required field missing" unless params[:required_field]
end
endThe live tool catalog — every action, parameter, example, and permission — lives at reference/auto/mcp-tools.md. It is auto-generated from Ai::Tools::PlatformApiToolRegistry.all_tools and regenerates via cd server && bundle exec rails mcp:generate_tool_catalog.
Do not inline action counts, tool class counts, or per-domain numbers in concept or guide docs. Always link to the catalog. Counts drift; the auto-generated reference is the source of truth.
concepts/agents-and-autonomy.md— how agents use MCP toolsconcepts/knowledge-and-memory.md— knowledge/memory toolsconcepts/chat-and-realtime.md— channels broadcasting tool execution eventsreference/auto/mcp-tools.md— live tool catalogguides/backend.md— adding tools
Tool registration follows a deterministic three-touch flow: drop a new tool class into server/app/services/ai/tools/, register the actions it exposes inside Ai::Tools::PlatformApiToolRegistry::TOOLS, then regenerate the catalog so downstream documentation and discovery surfaces (Claude Code's tools/list, the workspace concierge, the auto-generated reference) see the new actions. The registry is class-method dispatched — no autoload hook, no metaprogramming — so the only thing tying an action name to a tool is the explicit mapping in TOOLS.
-
Create
server/app/services/ai/tools/<feature>_tool.rbwithREQUIRED_PERMISSION(a string likeai.agents.read) andaction_definitionsreturning the per-action JSON Schema:# frozen_string_literal: true module Ai module Tools class FeatureTool < BaseTool REQUIRED_PERMISSION = "ai.feature.read" def self.action_definitions { "feature_do_thing" => { description: "Run the thing once", parameters: { id: { type: "string", required: true } } } } end protected def call(params) # validation → permission check → execution → audit log end end end end
-
Add to
Ai::Tools::PlatformApiToolRegistry::TOOLS— map every action name your class handles to the class string (the registry uses string class names so it can stay autoload-safe):"feature_do_thing" => "Ai::Tools::FeatureTool",
-
Run
cd server && bundle exec rails mcp:generate_tool_catalog— this introspects the registry, callsaction_definitionson each class, and rewritesdocs/reference/auto/mcp-tools.md. -
Verify your action appears in
reference/auto/mcp-tools.md— if the action is missing, the registry entry or theaction_definitionsmethod is the most likely cause.
For the in-line ## Adding a new tool walkthrough (with implementation patterns, audit log examples, deprecation flow), see the section above. Full walkthrough at guides/mcp-tool-development.md.
This concept consolidates content from:
docs/platform/MCP_INTEGRATION_GUIDE.mddocs/platform/MCP_CONFIGURATION.md
Last verified: 2026-06-03