[pending-judge] feat(DC-166 r2): MCP tool-set expansion + workflows double-prefix route fix (17 -> 28 tools)
Owner directive "Do it" (2026-10-09): expand the MCP tool set so the
"manage your entire infrastructure" claim is true of the product. 11 new
tools over EXISTING routes: calendar (events/add_source), security
(events/summary), license_status, capabilities, DNS add/remove record,
deploy_recipe, run_workflow, tailscale_status. Handlers verified against
the actual route declarations; credential TYPE passthrough and the DC-162
machine-only tools/call gate unchanged (tests included).
judge r1 (grade C, urn:ump:fyvivcvrphjm5wk227ujr2yzfcgcdvwekjmw6lvnqvjosqrjhtbq)
blocking issues — all fixed in this revision:
1. WORKFLOWS DOUBLE-PREFIX (real product bug the judge caught):
routes/workflows.js declared literal '/workflows/...' paths while
src/app.js mounts the router at /workflows — composed path was
/api/v1/workflows/workflows/..., which the SHIPPED dashboard bundle
(status/js/bundled-workflows.js calls single-prefix /api/v1/workflows/)
never hits: the workflows UI has been broken in production. Fixed by
making route paths RELATIVE to the mount (same class as the recipes
fix); run-workflow MCP path now matches. Also reordered /history
before /:workflowId/history so the literal segment is not captured as
an id (limit 100 vs 50 asserted in tests).
2. Verifier substring check replaced: NEW integration suite
(__tests__/mcp/mcp-core-dc166-integration.test.js) resolves EVERY MCP
tool call's complete mounted (method, path) against the declared route
patterns extracted from the owning route file (params matched as
patterns, comments excluded) — a double prefix or wrong method fails
the run. Plus real-composition dispatch through the actual workflows
router: POST /api/v1/workflows/:id/run reaches the run handler
(sentinel '"ran":true'), list/history handlers land correctly, and a
static guard fails any route file that re-declares its own mount name.
3. Both verbatim-command claims in the brief now name their file.
polish applied: calendar window-math test bounds tightened to exactly
~7 days (was a weak 6-hour lower bound); extra blank line before
tools/list removed; calendar_add_source schema gained the token property
the route already accepts (schema now matches the documented API).
Tests: 73 prior + 15 dc166 unit + 6 integration = 94 MCP tests green;
full suite 6 failed + 3697 passed == 3703 total (known-red billing
family, band-guarded; base-proof shows the same 6 red at a1b8fa3).
eslint: 0 errors on touched files (warnings count unchanged vs base).
This commit is contained in:
4 files changed
+578
-14
No files matched your search
@@ -192,6 +192,123 @@ const TOOLS = [
|
||||
description: 'List all hosts in the DashCaddy fleet (for multi-server management).',
|
||||
inputSchema: { type: 'object', properties: {} },
|
||||
},
|
||||
// ── Calendar (DC-166) ──
|
||||
{
|
||||
name: 'dashcaddy_calendar_events',
|
||||
description: 'List upcoming calendar events from configured sources (shared with the dashboard calendar widget). Useful for "what is on my schedule this week?"',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
days: { type: 'number', description: 'Horizon in days from now (default 14, max 60)' },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'dashcaddy_calendar_add_source',
|
||||
description: 'Add a calendar source: a CalDAV URL with user+pass or a token, or a read-only ICS feed URL. DashCaddy-deployed Radicale servers auto-register themselves; this tool is for external calendars.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string', description: 'Display name (e.g. "Personal")' },
|
||||
type: { type: 'string', enum: ['caldav', 'ics'], description: 'Source type' },
|
||||
url: { type: 'string', description: 'CalDAV base URL or .ics feed URL (https required)' },
|
||||
user: { type: 'string', description: 'CalDAV username (caldav with user+pass auth)' },
|
||||
pass: { type: 'string', description: 'CalDAV password (caldav with user+pass auth)' },
|
||||
token: { type: 'string', description: 'Auth token (CalDAV token auth or secret-bearing ICS URL)' },
|
||||
},
|
||||
required: ['name', 'type', 'url'],
|
||||
},
|
||||
},
|
||||
// ── Security Center (DC-166) ──
|
||||
{
|
||||
name: 'dashcaddy_security_events',
|
||||
description: 'Query the unified Security Center event pipeline (docker/caddy/dns/tailscale/audit/health sources, PII-masked). Useful for "any suspicious activity?" or "recent auth failures".',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
limit: { type: 'number', description: 'Max events (default 20)' },
|
||||
source_type: { type: 'string', description: 'Filter by source (caddy, docker, dns, tailscale, audit, health…)' },
|
||||
severity: { type: 'string', description: 'Filter by severity (info, warning, error, critical)' },
|
||||
since: { type: 'string', description: 'ISO datetime lower bound (optional)' },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'dashcaddy_security_summary',
|
||||
description: 'Security Center aggregate stats plus per-IP/per-vhost perimeter analytics. Useful for "which IPs are attacking me?"',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
withPerimeter: { type: 'boolean', description: 'Include perimeter rollups (default true)' },
|
||||
},
|
||||
},
|
||||
},
|
||||
// ── License + capabilities (DC-166) ──
|
||||
{
|
||||
name: 'dashcaddy_license_status',
|
||||
description: 'License status: active/inactive, tier, lifetime flag, days remaining, granted premium features, and the gated-feature catalog.',
|
||||
inputSchema: { type: 'object', properties: {} },
|
||||
},
|
||||
{
|
||||
name: 'dashcaddy_capabilities',
|
||||
description: 'Machine-readable capability manifest of the running build: version, app-template count + categories, MCP tool list, premium feature list, language count. Ground truth for "what can this DashCaddy do?"',
|
||||
inputSchema: { type: 'object', properties: {} },
|
||||
},
|
||||
// ── DNS write (DC-166) ──
|
||||
{
|
||||
name: 'dashcaddy_dns_add_record',
|
||||
description: 'Create a DNS A record via the configured DNS provider (Technitium/universal). Points a hostname at a machine without touching the dashboard.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
domain: { type: 'string', description: 'FQDN or hostname to add (e.g. "printer.home.example.com")' },
|
||||
ip: { type: 'string', description: 'IPv4 address for the A record' },
|
||||
ttl: { type: 'number', description: 'TTL seconds (optional, provider default)' },
|
||||
},
|
||||
required: ['domain', 'ip'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'dashcaddy_dns_remove_record',
|
||||
description: 'Delete a DNS record via the configured provider. Pass the identifying fields that dashcaddy_list_dns returned (domain, type, ipAddress).',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
domain: { type: 'string', description: 'Record name (e.g. "printer.home.example.com")' },
|
||||
type: { type: 'string', description: 'Record type (A, CNAME…; default A)' },
|
||||
ipAddress: { type: 'string', description: 'Record data (IP) to disambiguate' },
|
||||
},
|
||||
required: ['domain'],
|
||||
},
|
||||
},
|
||||
// ── Recipes & workflows (DC-166) ──
|
||||
{
|
||||
name: 'dashcaddy_deploy_recipe',
|
||||
description: 'Deploy a multi-container Recipe stack (premium) by recipe id with optional per-service overrides. List ids via dashcaddy_search_catalog-style GET /api/v1/recipes/templates.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
recipeId: { type: 'string', description: 'Recipe id (e.g. "media-stack")' },
|
||||
overrides: { type: 'object', description: 'Per-service config overrides (optional)', additionalProperties: true },
|
||||
},
|
||||
required: ['recipeId'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'dashcaddy_run_workflow',
|
||||
description: 'Run a bundled maintenance workflow (premium): auto-restart-on-crash, backup-before-update, health-check-on-interval, disk-space-alert, weekly-container-report.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: { workflowId: { type: 'string', description: 'Workflow id' } },
|
||||
required: ['workflowId'],
|
||||
},
|
||||
},
|
||||
// ── Tailscale (DC-166) ──
|
||||
{
|
||||
name: 'dashcaddy_tailscale_status',
|
||||
description: 'Tailscale integration status and enrolled devices (when the Tailscale feature is configured). Useful for checking VPN coverage of tailnet-only services.',
|
||||
inputSchema: { type: 'object', properties: {} },
|
||||
},
|
||||
];
|
||||
|
||||
const RESOURCES = [
|
||||
@@ -424,6 +541,85 @@ function createToolHandler({ baseUrl, apiKey, headers, fetchImpl, log }) {
|
||||
return apiCall('GET', '/fleet/hosts');
|
||||
}
|
||||
|
||||
// ── Calendar (DC-166) ──
|
||||
case 'dashcaddy_calendar_events': {
|
||||
// Route contract (routes/calendar.js): ISO start/end, capped 400-day
|
||||
// range. Translate the friendlier `days` horizon into that shape.
|
||||
const days = Math.min(Math.max(Number(args.days) || 14, 1), 60);
|
||||
const from = new Date();
|
||||
const to = new Date(from.getTime() + days * 86400000);
|
||||
const p = `/calendar/events?start=${encodeURIComponent(from.toISOString())}&end=${encodeURIComponent(to.toISOString())}`;
|
||||
return apiCall('GET', p);
|
||||
}
|
||||
|
||||
case 'dashcaddy_calendar_add_source': {
|
||||
return apiCall('POST', '/calendar/sources', {
|
||||
name: args.name, type: args.type, url: args.url,
|
||||
...(args.user ? { user: args.user } : {}),
|
||||
...(args.pass ? { pass: args.pass } : {}),
|
||||
...(args.token ? { token: args.token } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
// ── Security Center (DC-166) ──
|
||||
case 'dashcaddy_security_events': {
|
||||
const qs = new URLSearchParams();
|
||||
for (const k of ['limit', 'source_type', 'severity', 'since']) {
|
||||
if (args[k] !== undefined && args[k] !== null) qs.set(k, String(args[k]));
|
||||
}
|
||||
const q = qs.toString();
|
||||
return apiCall('GET', `/security/events${q ? `?${q}` : ''}`);
|
||||
}
|
||||
|
||||
case 'dashcaddy_security_summary': {
|
||||
const stats = await apiCall('GET', '/security/events/stats');
|
||||
if (args.withPerimeter === false) return stats;
|
||||
const peri = await apiCall('GET', '/security/events/perimeter');
|
||||
return { stats, perimeter: peri };
|
||||
}
|
||||
|
||||
// ── License + capabilities (DC-166) ──
|
||||
case 'dashcaddy_license_status': {
|
||||
return apiCall('GET', '/license/status');
|
||||
}
|
||||
|
||||
case 'dashcaddy_capabilities': {
|
||||
return apiCall('GET', '/capabilities');
|
||||
}
|
||||
|
||||
// ── DNS write (DC-166) ──
|
||||
case 'dashcaddy_dns_add_record': {
|
||||
return apiCall('POST', '/dns/record', {
|
||||
domain: args.domain, ip: args.ip,
|
||||
...(args.ttl !== undefined ? { ttl: args.ttl } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
case 'dashcaddy_dns_remove_record': {
|
||||
const qs = new URLSearchParams();
|
||||
qs.set('domain', String(args.domain));
|
||||
qs.set('type', String(args.type || 'A'));
|
||||
if (args.ipAddress !== undefined) qs.set('ipAddress', String(args.ipAddress));
|
||||
return apiCall('DELETE', `/dns/record?${qs.toString()}`);
|
||||
}
|
||||
|
||||
// ── Recipes & workflows (DC-166) ──
|
||||
case 'dashcaddy_deploy_recipe': {
|
||||
return apiCall('POST', '/recipes/deploy', {
|
||||
recipeId: args.recipeId,
|
||||
...(args.overrides ? { config: args.overrides } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
case 'dashcaddy_run_workflow': {
|
||||
return apiCall('POST', `/workflows/${encodeURIComponent(args.workflowId)}/run`, {});
|
||||
}
|
||||
|
||||
// ── Tailscale (DC-166) ──
|
||||
case 'dashcaddy_tailscale_status': {
|
||||
return apiCall('GET', '/tailscale/status');
|
||||
}
|
||||
|
||||
default:
|
||||
return { error: true, message: `Unknown tool: ${name}` };
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user