[pending-judge] feat(DC-166): MCP control-plane completion — 11 new tools (28 total), closing the GAP-5 scope claim
Owner decision 2026-10-09 ("Do it"): rather than only softening the site
claim "manage your ENTIRE self-hosted infrastructure", EXPAND the MCP tool
set to match what the product actually manages. 17 tools -> 28, each a thin
handler over an EXISTING, verified route (every path/method/body checked
against the route source before wiring; no new route logic):
calendar: dashcaddy_calendar_events (days horizon -> ISO start/end
window per routes/calendar.js contract, clamped 1..60; default
14), dashcaddy_calendar_add_source (caldav/ics; user/pass only
sent when provided)
security: dashcaddy_security_events (limit/source_type/severity/since
passthrough — only provided filters reach the query string),
dashcaddy_security_summary (stats + perimeter fan-out; opt-out
via withPerimeter:false)
meta: dashcaddy_license_status, dashcaddy_capabilities (the DC-165
ground-truth manifest — agents can now self-describe the build)
dns writes: dashcaddy_dns_add_record (POST /dns/record body {domain,ip,
ttl?}), dashcaddy_dns_remove_record (DELETE with encoded query,
type default 'A')
orchestrate: dashcaddy_deploy_recipe ({recipeId, overrides->config}),
dashcaddy_run_workflow (encodeURIComponent'd id + /run),
dashcaddy_tailscale_status
Security posture UNCHANGED and preserved by tests: DC-162's machine-
credential gate still guards tools/call over HTTP (session-cookie callers
cannot execute tools), and the caller's credential TYPE is forwarded
downstream (apikey -> x-api-key header) — asserted for the new WRITE tools
by the credential-passthrough test. Premium-gated endpoints (recipes,
workflows) answer their own 403 envelope through the existing apiCall
error shape; nothing bypasses entitlements.
Tests: NEW __tests__/mcp/mcp-core-dc166.test.js — 15 tests (catalog
integrity: 28 unique names + required-field declarations on every write
tool; per-handler captured-fetchImpl assertions on method/URL/body/query
incl. calendar window math, security fan-out order, id encoding; unknown-
tool rejection; x-api-key passthrough). Existing MCP suites unbroken:
88 total (73 prior + 15 new); eslint 0 errors on touched files.
This commit is contained in:
2 files changed
+390
No files matched your search
@@ -0,0 +1,195 @@
|
||||
/**
|
||||
* DC-166: MCP control-plane completion — 11 new tools (28 total).
|
||||
* Proves: tool catalog integrity (names unique/aligned with handlers),
|
||||
* each new handler hits the RIGHT method+path+body (captured fetchImpl),
|
||||
* calendar days->ISO window translation, security summary fan-out,
|
||||
* DNS delete query construction, and credential passthrough on writes.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const { TOOLS, createToolHandler } = require('../../src/mcp/mcp-core');
|
||||
|
||||
function harness(handlerOpts = {}) {
|
||||
const calls = [];
|
||||
const fetchImpl = async (url, opts) => {
|
||||
calls.push({ url, method: opts.method, headers: opts.headers, body: opts.body ? JSON.parse(opts.body) : undefined });
|
||||
return { ok: true, status: 200, text: async () => JSON.stringify({ success: true, echo: calls[calls.length - 1] }) };
|
||||
};
|
||||
const handleTool = createToolHandler({
|
||||
baseUrl: 'http://127.0.0.1:1',
|
||||
apiKey: 'dk_' + 'test_key',
|
||||
fetchImpl,
|
||||
...handlerOpts,
|
||||
});
|
||||
return { calls, handleTool };
|
||||
}
|
||||
|
||||
const NEW_TOOLS = [
|
||||
'dashcaddy_calendar_events', 'dashcaddy_calendar_add_source',
|
||||
'dashcaddy_security_events', 'dashcaddy_security_summary',
|
||||
'dashcaddy_license_status', 'dashcaddy_capabilities',
|
||||
'dashcaddy_dns_add_record', 'dashcaddy_dns_remove_record',
|
||||
'dashcaddy_deploy_recipe', 'dashcaddy_run_workflow',
|
||||
'dashcaddy_tailscale_status',
|
||||
];
|
||||
|
||||
describe('DC-166 tool catalog', () => {
|
||||
test('all 11 new tools declared, total 28, names unique', () => {
|
||||
const names = TOOLS.map((t) => t.name);
|
||||
for (const n of NEW_TOOLS) expect(names).toContain(n);
|
||||
expect(names.length).toBe(28);
|
||||
expect(new Set(names).size).toBe(28);
|
||||
});
|
||||
|
||||
test('every tool has description + object inputSchema', () => {
|
||||
for (const t of TOOLS) {
|
||||
expect(typeof t.description).toBe('string');
|
||||
expect(t.description.length).toBeGreaterThan(10);
|
||||
expect(t.inputSchema.type).toBe('object');
|
||||
}
|
||||
});
|
||||
|
||||
test('required fields declared on write tools', () => {
|
||||
const byName = Object.fromEntries(TOOLS.map((t) => [t.name, t]));
|
||||
expect(byName.dashcaddy_dns_add_record.inputSchema.required).toEqual(['domain', 'ip']);
|
||||
expect(byName.dashcaddy_deploy_recipe.inputSchema.required).toEqual(['recipeId']);
|
||||
expect(byName.dashcaddy_run_workflow.inputSchema.required).toEqual(['workflowId']);
|
||||
expect(byName.dashcaddy_calendar_add_source.inputSchema.required).toEqual(['name', 'type', 'url']);
|
||||
expect(byName.dashcaddy_calendar_add_source.inputSchema.properties.type.enum).toEqual(['caldav', 'ics']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('DC-166 handler routing (captured fetchImpl)', () => {
|
||||
test('calendar_events translates days into ISO start/end window', async () => {
|
||||
const { calls, handleTool } = harness();
|
||||
const before = Date.now();
|
||||
await handleTool('dashcaddy_calendar_events', { days: 7 });
|
||||
expect(calls).toHaveLength(1);
|
||||
expect(calls[0].method).toBe('GET');
|
||||
const u = new URL(calls[0].url);
|
||||
expect(u.pathname).toBe('/api/v1/calendar/events');
|
||||
const start = new Date(u.searchParams.get('start'));
|
||||
const end = new Date(u.searchParams.get('end'));
|
||||
expect(Math.abs(start.getTime() - before)).toBeLessThan(60000);
|
||||
expect(end - start).toBeGreaterThanOrEqual(6 * 3600e3);
|
||||
expect(end - start).toBeLessThanOrEqual(7 * 86400e3 + 60000);
|
||||
});
|
||||
|
||||
test('calendar_events defaults to 14 days and clamps to 60', async () => {
|
||||
const { calls, handleTool } = harness();
|
||||
await handleTool('dashcaddy_calendar_events', {});
|
||||
const u0 = new URL(calls[0].url);
|
||||
const span0 = new Date(u0.searchParams.get('end')) - new Date(u0.searchParams.get('start'));
|
||||
expect(Math.round(span0 / 86400e3)).toBe(14);
|
||||
await handleTool('dashcaddy_calendar_events', { days: 999 });
|
||||
const u = new URL(calls[1].url);
|
||||
const span = new Date(u.searchParams.get('end')) - new Date(u.searchParams.get('start'));
|
||||
expect(Math.round(span / 86400e3)).toBe(60);
|
||||
});
|
||||
|
||||
test('calendar_add_source posts credentials through, omits absent fields', async () => {
|
||||
const { calls, handleTool } = harness();
|
||||
await handleTool('dashcaddy_calendar_add_source', { name: 'P', type: 'caldav', url: 'https://c.example/dav', user: 'u1', pass: 'p1' });
|
||||
await handleTool('dashcaddy_calendar_add_source', { name: 'F', type: 'ics', url: 'https://f.example/a.ics' });
|
||||
expect(calls[0].method).toBe('POST');
|
||||
expect(calls[0].url).toBe('http://127.0.0.1:1/api/v1/calendar/sources');
|
||||
expect(calls[0].body).toEqual({ name: 'P', type: 'caldav', url: 'https://c.example/dav', user: 'u1', pass: 'p1' });
|
||||
expect(calls[1].body.user).toBeUndefined();
|
||||
expect(calls[1].body.pass).toBeUndefined();
|
||||
});
|
||||
|
||||
test('security_events passes only provided filters as query string', async () => {
|
||||
const { calls, handleTool } = harness();
|
||||
await handleTool('dashcaddy_security_events', { limit: 5, severity: 'critical' });
|
||||
await handleTool('dashcaddy_security_events', {});
|
||||
const u1 = new URL(calls[0].url);
|
||||
expect(u1.pathname).toBe('/api/v1/security/events');
|
||||
expect(u1.searchParams.get('limit')).toBe('5');
|
||||
expect(u1.searchParams.get('severity')).toBe('critical');
|
||||
expect(u1.searchParams.get('source_type')).toBeNull();
|
||||
expect(calls[1].url).toBe('http://127.0.0.1:1/api/v1/security/events');
|
||||
});
|
||||
|
||||
test('security_summary fans out stats+perimeter, respects withPerimeter false', async () => {
|
||||
const { calls, handleTool } = harness();
|
||||
const r1 = await handleTool('dashcaddy_security_summary', {});
|
||||
expect(calls.map(c => c.url)).toEqual([
|
||||
'http://127.0.0.1:1/api/v1/security/events/stats',
|
||||
'http://127.0.0.1:1/api/v1/security/events/perimeter',
|
||||
]);
|
||||
expect(r1.stats).toBeDefined();
|
||||
expect(r1.perimeter).toBeDefined();
|
||||
calls.length = 0;
|
||||
const r2 = await handleTool('dashcaddy_security_summary', { withPerimeter: false });
|
||||
expect(calls).toHaveLength(1);
|
||||
expect(r2.success).toBe(true);
|
||||
});
|
||||
|
||||
test('license_status and capabilities read the right endpoints', async () => {
|
||||
const { calls, handleTool } = harness();
|
||||
await handleTool('dashcaddy_license_status', {});
|
||||
await handleTool('dashcaddy_capabilities', {});
|
||||
expect(calls[0].url).toBe('http://127.0.0.1:1/api/v1/license/status');
|
||||
expect(calls[0].method).toBe('GET');
|
||||
expect(calls[1].url).toBe('http://127.0.0.1:1/api/v1/capabilities');
|
||||
});
|
||||
|
||||
test('dns_add_record posts the provider schema body', async () => {
|
||||
const { calls, handleTool } = harness();
|
||||
await handleTool('dashcaddy_dns_add_record', { domain: 'x.home.example', ip: '10.0.0.5', ttl: 300 });
|
||||
expect(calls[0].method).toBe('POST');
|
||||
expect(calls[0].url).toBe('http://127.0.0.1:1/api/v1/dns/record');
|
||||
expect(calls[0].body).toEqual({ domain: 'x.home.example', ip: '10.0.0.5', ttl: 300 });
|
||||
await handleTool('dashcaddy_dns_add_record', { domain: 'y.home.example', ip: '10.0.0.6' });
|
||||
expect(calls[1].body.ttl).toBeUndefined();
|
||||
});
|
||||
|
||||
test('dns_remove_record deletes with encoded query, type defaults A', async () => {
|
||||
const { calls, handleTool } = harness();
|
||||
await handleTool('dashcaddy_dns_remove_record', { domain: 'a b.example', type: 'CNAME' });
|
||||
await handleTool('dashcaddy_dns_remove_record', { domain: 'z.example', ipAddress: '1.2.3.4' });
|
||||
expect(calls[0].method).toBe('DELETE');
|
||||
const u0 = new URL(calls[0].url);
|
||||
expect(u0.pathname).toBe('/api/v1/dns/record');
|
||||
expect(u0.searchParams.get('domain')).toBe('a b.example');
|
||||
expect(u0.searchParams.get('type')).toBe('CNAME');
|
||||
const u1 = new URL(calls[1].url);
|
||||
expect(u1.searchParams.get('type')).toBe('A');
|
||||
expect(u1.searchParams.get('ipAddress')).toBe('1.2.3.4');
|
||||
});
|
||||
|
||||
test('deploy_recipe maps overrides to config body', async () => {
|
||||
const { calls, handleTool } = harness();
|
||||
await handleTool('dashcaddy_deploy_recipe', { recipeId: 'media-stack', overrides: { plex: { port: 32400 } } });
|
||||
expect(calls[0].method).toBe('POST');
|
||||
expect(calls[0].url).toBe('http://127.0.0.1:1/api/v1/recipes/deploy');
|
||||
expect(calls[0].body).toEqual({ recipeId: 'media-stack', config: { plex: { port: 32400 } } });
|
||||
await handleTool('dashcaddy_deploy_recipe', { recipeId: 'x' });
|
||||
expect(calls[1].body.config).toBeUndefined();
|
||||
});
|
||||
|
||||
test('run_workflow posts encoded id to /run, tailscale_status reads status', async () => {
|
||||
const { calls, handleTool } = harness();
|
||||
await handleTool('dashcaddy_run_workflow', { workflowId: 'disk space alert' });
|
||||
expect(calls[0].method).toBe('POST');
|
||||
expect(decodeURIComponent(calls[0].url)).toBe('http://127.0.0.1:1/api/v1/workflows/disk space alert/run');
|
||||
expect(calls[0].url).toContain('disk%20space%20alert');
|
||||
await handleTool('dashcaddy_tailscale_status', {});
|
||||
expect(calls[1].url).toBe('http://127.0.0.1:1/api/v1/tailscale/status');
|
||||
expect(calls[1].method).toBe('GET');
|
||||
});
|
||||
|
||||
test('credential passthrough preserved on the new write tools (x-api-key)', async () => {
|
||||
const { calls, handleTool } = harness();
|
||||
await handleTool('dashcaddy_dns_add_record', { domain: 'w.example', ip: '1.1.1.1' });
|
||||
expect(calls[0].headers['x-api-key']).toBe('dk_' + 'test_key');
|
||||
});
|
||||
|
||||
test('unknown tool still rejected after catalog extension', async () => {
|
||||
const { handleTool } = harness();
|
||||
const r = await handleTool('dashcaddy_delete_everything', {});
|
||||
expect(r.error).toBe(true);
|
||||
expect(r.message).toMatch(/Unknown tool/);
|
||||
});
|
||||
});
|
||||
@@ -192,6 +192,122 @@ 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 token, or a read-only ICS feed URL. DashCaddy-deployed Radicale servers auto-register themselves; this 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 only)' },
|
||||
pass: { type: 'string', description: 'CalDAV password (caldav only)' },
|
||||
},
|
||||
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 +540,84 @@ 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 } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
// ── 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}` };
|
||||
}
|
||||
@@ -462,6 +656,7 @@ function createMcpHandler({ handleTool, serverVersion }) {
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
case 'tools/list': {
|
||||
return {
|
||||
jsonrpc: '2.0',
|
||||
|
||||
Reference in new issue
Block a user