refactor: scaffold module registries and default-module layout

Additive change — existing code paths still run via inline fallbacks.
Prepares core for per-module extractions in PR #3 onward.

Four registries added with empty defaults:
  - delivery action handlers (delivery.ts)
  - router inbound gate (router.ts)
  - response dispatcher (index.ts)
  - MCP tool self-registration (container/agent-runner/src/mcp-tools/server.ts)

Default modules moved to src/modules/ for signaling:
  - src/modules/typing/       (extracted from delivery.ts)
  - src/modules/mount-security/ (moved from src/mount-security.ts)

Both are imported directly by core — no hook, no registry. Removal
requires editing core imports.

Migrator now keys applied rows by name (uniqueness) so module
migrations can pick arbitrary version numbers. Stored version column
is auto-assigned as an applied-order sequence.

sqlite_master guards added around core calls into module-owned tables
(user_roles, agent_destinations, pending_questions). No-ops today;
load-bearing after the owning modules are extracted.

MODULE-HOOK markers placed at scheduling's two skill-edit sites
(host-sweep.ts recurrence call, poll-loop.ts pre-task gate). PR #4
replaces the marked blocks when scheduling moves to its module.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gavrielc
2026-04-18 14:46:19 +03:00
parent 1888ecc1e9
commit 4202041d0b
19 changed files with 480 additions and 234 deletions

View File

@@ -9,6 +9,7 @@
* (see mcp-tools/index.ts). The host re-checks permission on receive.
*/
import { writeMessageOut } from '../db/messages-out.js';
import { registerTools } from './server.js';
import type { McpToolDefinition } from './types.js';
function log(msg: string): void {
@@ -62,4 +63,4 @@ export const createAgent: McpToolDefinition = {
},
};
export const agentTools: McpToolDefinition[] = [createAgent];
registerTools([createAgent]);

View File

@@ -12,6 +12,7 @@ import path from 'path';
import { findByName, getAllDestinations } from '../destinations.js';
import { getMessageIdBySeq, getRoutingBySeq, writeMessageOut } from '../db/messages-out.js';
import { getSessionRouting } from '../db/session-routing.js';
import { registerTools } from './server.js';
import type { McpToolDefinition } from './types.js';
function log(msg: string): void {
@@ -258,4 +259,4 @@ export const addReaction: McpToolDefinition = {
},
};
export const coreTools: McpToolDefinition[] = [sendMessage, sendFile, editMessage, addReaction];
registerTools([sendMessage, sendFile, editMessage, addReaction]);

View File

@@ -1,59 +1,21 @@
/**
* MCP tools barrel — collects all tool modules and starts the server.
* MCP tools barrel — imports each tool module for its side-effect
* `registerTools([...])` call, then starts the MCP server.
*
* Each module exports a McpToolDefinition[] array. This file registers
* them all with the MCP server. Adding a new tool module requires only
* importing it here and spreading its tools array.
* Adding a new tool module: create the file, call `registerTools([...])`
* at module scope, and append the import here. No central list.
*/
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
import type { McpToolDefinition } from './types.js';
import { coreTools } from './core.js';
import { schedulingTools } from './scheduling.js';
import { interactiveTools } from './interactive.js';
import { agentTools } from './agents.js';
import { selfModTools } from './self-mod.js';
import './core.js';
import './scheduling.js';
import './interactive.js';
import './agents.js';
import './self-mod.js';
import { startMcpServer } from './server.js';
function log(msg: string): void {
console.error(`[mcp-tools] ${msg}`);
}
const allTools: McpToolDefinition[] = [
...coreTools,
...schedulingTools,
...interactiveTools,
...agentTools,
...selfModTools,
];
const toolMap = new Map<string, McpToolDefinition>();
for (const t of allTools) {
toolMap.set(t.tool.name, t);
}
async function startMcpServer(): Promise<void> {
const server = new Server({ name: 'nanoclaw', version: '2.0.0' }, { capabilities: { tools: {} } });
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: allTools.map((t) => t.tool),
}));
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
const tool = toolMap.get(name);
if (!tool) {
return { content: [{ type: 'text', text: `Unknown tool: ${name}` }] };
}
return tool.handler(args ?? {});
});
const transport = new StdioServerTransport();
await server.connect(transport);
log(`MCP server started with ${allTools.length} tools: ${allTools.map((t) => t.tool.name).join(', ')}`);
}
startMcpServer().catch((err) => {
log(`MCP server error: ${err instanceof Error ? err.message : String(err)}`);
process.exit(1);

View File

@@ -7,6 +7,7 @@
import { findQuestionResponse, markCompleted } from '../db/messages-in.js';
import { writeMessageOut } from '../db/messages-out.js';
import { getSessionRouting } from '../db/session-routing.js';
import { registerTools } from './server.js';
import type { McpToolDefinition } from './types.js';
function log(msg: string): void {
@@ -165,4 +166,4 @@ export const sendCard: McpToolDefinition = {
},
};
export const interactiveTools: McpToolDefinition[] = [askUserQuestion, sendCard];
registerTools([askUserQuestion, sendCard]);

View File

@@ -8,6 +8,7 @@
import { getInboundDb } from '../db/connection.js';
import { writeMessageOut } from '../db/messages-out.js';
import { getSessionRouting } from '../db/session-routing.js';
import { registerTools } from './server.js';
import type { McpToolDefinition } from './types.js';
function log(msg: string): void {
@@ -265,4 +266,4 @@ export const updateTask: McpToolDefinition = {
},
};
export const schedulingTools: McpToolDefinition[] = [scheduleTask, listTasks, updateTask, cancelTask, pauseTask, resumeTask];
registerTools([scheduleTask, listTasks, updateTask, cancelTask, pauseTask, resumeTask]);

View File

@@ -9,6 +9,7 @@
* the host side (defense in depth).
*/
import { writeMessageOut } from '../db/messages-out.js';
import { registerTools } from './server.js';
import type { McpToolDefinition } from './types.js';
function log(msg: string): void {
@@ -140,4 +141,4 @@ export const requestRebuild: McpToolDefinition = {
},
};
export const selfModTools: McpToolDefinition[] = [installPackages, addMcpServer, requestRebuild];
registerTools([installPackages, addMcpServer, requestRebuild]);

View File

@@ -0,0 +1,54 @@
/**
* MCP server bootstrap + tool self-registration.
*
* Each tool module calls `registerTools([...])` at import time. The
* barrel (`index.ts`) imports every tool module for side effects, then
* calls `startMcpServer()` which uses whatever was registered.
*
* Default when only `core.ts` is imported: the core `send_message` /
* `send_file` / `edit_message` / `add_reaction` tools are available.
*/
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
import type { McpToolDefinition } from './types.js';
function log(msg: string): void {
console.error(`[mcp-tools] ${msg}`);
}
const allTools: McpToolDefinition[] = [];
const toolMap = new Map<string, McpToolDefinition>();
export function registerTools(tools: McpToolDefinition[]): void {
for (const t of tools) {
if (toolMap.has(t.tool.name)) {
log(`Warning: tool "${t.tool.name}" already registered, skipping duplicate`);
continue;
}
allTools.push(t);
toolMap.set(t.tool.name, t);
}
}
export async function startMcpServer(): Promise<void> {
const server = new Server({ name: 'nanoclaw', version: '2.0.0' }, { capabilities: { tools: {} } });
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: allTools.map((t) => t.tool),
}));
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
const tool = toolMap.get(name);
if (!tool) {
return { content: [{ type: 'text', text: `Unknown tool: ${name}` }] };
}
return tool.handler(args ?? {});
});
const transport = new StdioServerTransport();
await server.connect(transport);
log(`MCP server started with ${allTools.length} tools: ${allTools.map((t) => t.tool.name).join(', ')}`);
}

View File

@@ -156,11 +156,18 @@ export async function runPollLoop(config: PollLoopConfig): Promise<void> {
// Pre-task scripts: for any task rows with a `script`, run it before the
// provider call. Scripts returning wakeAgent=false (or erroring) gate
// their own task row only — surviving messages still go to the agent.
//
// MODULE-HOOK:scheduling-pre-task:start
// When scheduling is extracted (PR #4), `applyPreTaskScripts` moves
// to the scheduling module and the `/add-scheduling` skill replaces
// this block with a call to the module. Without scheduling installed,
// the block is empty (no script gating) and `keep = normalMessages`.
const { keep, skipped } = await applyPreTaskScripts(normalMessages);
if (skipped.length > 0) {
markCompleted(skipped);
log(`Pre-task script skipped ${skipped.length} task(s): ${skipped.join(', ')}`);
}
// MODULE-HOOK:scheduling-pre-task:end
if (keep.length === 0) {
log(`All ${normalMessages.length} non-command message(s) gated by script, skipping query`);