Problem
The MCP server currently registers all 29 tools unconditionally for every invocation. OpenClaw gates owner-only tools (cron management, gateway admin) behind a senderIsOwner flag resolved from command authorization, and applies tool profiles (minimal/coding/messaging/full) to control which tool categories are available. The MCP server needs the same gating.
Without this, a non-owner channel user can instruct the agent to modify cron schedules or restart the gateway via MCP tools — actions that should be restricted to the bot owner.
Current State
ChannelBridge passes no auth context
ChannelBridge.#buildMcpConfig() (src/middleware/channel-bridge.ts ~line 177) sets env vars for the MCP server but includes no security context:
#buildMcpConfig(message, sessionKey, sideEffectsFile) {
return {
remoteclaw: {
command: "node",
args: [this.#mcpServerPath],
env: {
REMOTECLAW_GATEWAY_URL: ...,
REMOTECLAW_GATEWAY_TOKEN: ...,
REMOTECLAW_SESSION_KEY: ...,
REMOTECLAW_SIDE_EFFECTS_FILE: ...,
REMOTECLAW_CHANNEL: ...,
REMOTECLAW_ACCOUNT_ID: ...,
REMOTECLAW_TO: ...,
// No REMOTECLAW_SENDER_IS_OWNER
// No REMOTECLAW_TOOL_PROFILE
},
},
};
}
MCP server registers all tools unconditionally
registerAllTools() (src/middleware/mcp-tools.ts) calls all 4 registration functions without filtering:
export function registerAllTools(server: McpServer, ctx: McpHandlerContext): void {
registerSessionTools(server, ctx); // 7 tools
registerMessageTools(server, ctx); // 10 tools
registerCronTools(server, ctx); // 7 tools — owner-only
registerGatewayTools(server, ctx); // 5 tools — owner-only (write), read OK
}
McpHandlerContext has no auth fields
McpHandlerContext (src/middleware/mcp-handlers/context.ts) currently only carries routing fields (gateway URL, session key, channel info).
Where senderIsOwner comes from
resolveCommandAuthorization() in src/auto-reply/command-auth.ts resolves senderIsOwner: boolean from the commands.ownerAllowFrom config. This is called in the auto-reply pipeline before the agent runs. The cron dispatch always uses senderIsOwner: false (cron agents don't self-modify).
Where tool profiles come from
resolveToolProfilePolicy() in src/agents/tool-policy-shared.ts resolves a ToolProfileId ("minimal" | "coding" | "messaging" | "full") from agent config. OpenClaw uses this to control which tool categories are registered.
Which tools are owner-only
From src/agents/tools/:
cron-tool.ts: ownerOnly: true (cron_add, cron_list, cron_remove, cron_update, cron_get, cron_pause, cron_resume)
gateway-tool.ts: ownerOnly: true (gateway_restart, gateway_config_apply, gateway_config_get, gateway_config_schema, gateway_status)
Required Changes
1. Add auth fields to McpHandlerContext (~3 lines)
In src/middleware/mcp-handlers/context.ts, add:
export type McpHandlerContext = {
// ... existing fields ...
/** Whether the message sender is the bot owner. */
senderIsOwner: boolean;
/** Tool profile controlling which tool categories are available. */
toolProfile: string; // "minimal" | "coding" | "messaging" | "full"
};
2. Pass auth env vars from ChannelBridge (~3 lines)
In ChannelBridge.#buildMcpConfig(), add env vars:
env: {
// ... existing vars ...
REMOTECLAW_SENDER_IS_OWNER: String(senderIsOwner),
REMOTECLAW_TOOL_PROFILE: toolProfile,
},
This requires ChannelBridge.handle() to accept or resolve senderIsOwner and toolProfile. The caller (each dispatch site) provides these.
3. Read auth env vars in MCP server startup (~3 lines)
In createContext() (src/middleware/mcp-server.ts), add:
senderIsOwner: process.env.REMOTECLAW_SENDER_IS_OWNER === "true",
toolProfile: process.env.REMOTECLAW_TOOL_PROFILE || "full",
4. Filter tool registration in registerAllTools() (~10 lines)
In src/middleware/mcp-tools.ts, gate registrations:
export function registerAllTools(server: McpServer, ctx: McpHandlerContext): void {
registerSessionTools(server, ctx);
registerMessageTools(server, ctx);
if (ctx.senderIsOwner) {
registerCronTools(server, ctx);
registerGatewayTools(server, ctx);
}
// Tool profile filtering can be layered on top if needed
}
5. Thread senderIsOwner through dispatch sites (~5 lines each)
Each dispatch site that calls ChannelBridge.handle() must pass the security context:
- Auto-reply (
src/auto-reply/reply/agent-runner-execution.ts): Already resolves commandAuth.senderIsOwner — pass it through.
- CLI command (
src/commands/agent.ts): CLI user is always the owner → senderIsOwner: true.
- Cron (
src/cron/isolated-agent/run.ts): Cron agents are never owner → senderIsOwner: false.
- Voice-call (
extensions/voice-call/src/core-bridge.ts): Check extension context for owner status.
6. Add unit tests (~30 lines)
- Test that
registerAllTools with senderIsOwner: false does NOT register cron/gateway tools
- Test that
registerAllTools with senderIsOwner: true registers all tools
- Test that
ChannelBridge.#buildMcpConfig() includes REMOTECLAW_SENDER_IS_OWNER env var
- Test that
createContext() parses REMOTECLAW_SENDER_IS_OWNER correctly
Acceptance Criteria
Files to Touch
| File |
Change |
src/middleware/mcp-handlers/context.ts |
Add senderIsOwner and toolProfile fields |
src/middleware/mcp-server.ts |
Read new env vars in createContext() |
src/middleware/mcp-tools.ts |
Gate cron/gateway tool registration on senderIsOwner |
src/middleware/channel-bridge.ts |
Accept auth context, pass as env vars to MCP server |
src/middleware/channel-bridge.test.ts |
Test auth env var propagation |
src/middleware/types.ts |
Optional: add auth context to ChannelBridge constructor or handle() params |
src/middleware/mcp-tools.test.ts |
Test owner/non-owner tool filtering |
src/auto-reply/reply/agent-runner-execution.ts |
Pass senderIsOwner from command auth |
src/commands/agent.ts |
Pass senderIsOwner: true |
src/cron/isolated-agent/run.ts |
Pass senderIsOwner: false |
extensions/voice-call/src/core-bridge.ts |
Pass senderIsOwner from extension context |
Context
- OpenClaw tool gating:
src/agents/tool-policy.ts — applyOwnerOnlyToolPolicy() wraps owner-only tools
- Owner-only flag:
src/agents/tools/cron-tool.ts (ownerOnly: true), src/agents/tools/gateway-tool.ts (ownerOnly: true)
- Command auth:
src/auto-reply/command-auth.ts — resolveCommandAuthorization() returns { senderIsOwner: boolean, ... }
Problem
The MCP server currently registers all 29 tools unconditionally for every invocation. OpenClaw gates owner-only tools (cron management, gateway admin) behind a
senderIsOwnerflag resolved from command authorization, and applies tool profiles (minimal/coding/messaging/full) to control which tool categories are available. The MCP server needs the same gating.Without this, a non-owner channel user can instruct the agent to modify cron schedules or restart the gateway via MCP tools — actions that should be restricted to the bot owner.
Current State
ChannelBridge passes no auth context
ChannelBridge.#buildMcpConfig()(src/middleware/channel-bridge.ts~line 177) sets env vars for the MCP server but includes no security context:MCP server registers all tools unconditionally
registerAllTools()(src/middleware/mcp-tools.ts) calls all 4 registration functions without filtering:McpHandlerContext has no auth fields
McpHandlerContext(src/middleware/mcp-handlers/context.ts) currently only carries routing fields (gateway URL, session key, channel info).Where senderIsOwner comes from
resolveCommandAuthorization()insrc/auto-reply/command-auth.tsresolvessenderIsOwner: booleanfrom thecommands.ownerAllowFromconfig. This is called in the auto-reply pipeline before the agent runs. The cron dispatch always usessenderIsOwner: false(cron agents don't self-modify).Where tool profiles come from
resolveToolProfilePolicy()insrc/agents/tool-policy-shared.tsresolves aToolProfileId("minimal" | "coding" | "messaging" | "full") from agent config. OpenClaw uses this to control which tool categories are registered.Which tools are owner-only
From
src/agents/tools/:cron-tool.ts:ownerOnly: true(cron_add, cron_list, cron_remove, cron_update, cron_get, cron_pause, cron_resume)gateway-tool.ts:ownerOnly: true(gateway_restart, gateway_config_apply, gateway_config_get, gateway_config_schema, gateway_status)Required Changes
1. Add auth fields to
McpHandlerContext(~3 lines)In
src/middleware/mcp-handlers/context.ts, add:2. Pass auth env vars from ChannelBridge (~3 lines)
In
ChannelBridge.#buildMcpConfig(), add env vars:This requires
ChannelBridge.handle()to accept or resolvesenderIsOwnerandtoolProfile. The caller (each dispatch site) provides these.3. Read auth env vars in MCP server startup (~3 lines)
In
createContext()(src/middleware/mcp-server.ts), add:4. Filter tool registration in
registerAllTools()(~10 lines)In
src/middleware/mcp-tools.ts, gate registrations:5. Thread
senderIsOwnerthrough dispatch sites (~5 lines each)Each dispatch site that calls
ChannelBridge.handle()must pass the security context:src/auto-reply/reply/agent-runner-execution.ts): Already resolvescommandAuth.senderIsOwner— pass it through.src/commands/agent.ts): CLI user is always the owner →senderIsOwner: true.src/cron/isolated-agent/run.ts): Cron agents are never owner →senderIsOwner: false.extensions/voice-call/src/core-bridge.ts): Check extension context for owner status.6. Add unit tests (~30 lines)
registerAllToolswithsenderIsOwner: falsedoes NOT register cron/gateway toolsregisterAllToolswithsenderIsOwner: trueregisters all toolsChannelBridge.#buildMcpConfig()includesREMOTECLAW_SENDER_IS_OWNERenv varcreateContext()parsesREMOTECLAW_SENDER_IS_OWNERcorrectlyAcceptance Criteria
senderIsOwnerflag is propagated from dispatch sites → ChannelBridge → MCP server env →McpHandlerContextsenderIsOwnerfromresolveCommandAuthorization()senderIsOwner: true(CLI user is always owner)senderIsOwner: false(cron agents don't self-modify)pnpm buildpassesFiles to Touch
src/middleware/mcp-handlers/context.tssenderIsOwnerandtoolProfilefieldssrc/middleware/mcp-server.tscreateContext()src/middleware/mcp-tools.tssenderIsOwnersrc/middleware/channel-bridge.tssrc/middleware/channel-bridge.test.tssrc/middleware/types.tsChannelBridgeconstructor orhandle()paramssrc/middleware/mcp-tools.test.tssrc/auto-reply/reply/agent-runner-execution.tssenderIsOwnerfrom command authsrc/commands/agent.tssenderIsOwner: truesrc/cron/isolated-agent/run.tssenderIsOwner: falseextensions/voice-call/src/core-bridge.tssenderIsOwnerfrom extension contextContext
src/agents/tool-policy.ts—applyOwnerOnlyToolPolicy()wraps owner-only toolssrc/agents/tools/cron-tool.ts(ownerOnly: true),src/agents/tools/gateway-tool.ts(ownerOnly: true)src/auto-reply/command-auth.ts—resolveCommandAuthorization()returns{ senderIsOwner: boolean, ... }