Netx tools: load from netx remote /mcp, add per-tool toggles

- Drop the TypeScript netx tool implementations and the hard-coded
  TOOLS_BY_GROUP list; register netx__* tools from the server's
  tools/list (group from _meta.netx.group) and forward tools/call.
- Thin Streamable HTTP client on standard fetch (fetchImpl injectable);
  cached catalog when netx is unreachable, refresh to reconnect.
- disabledTools (deny list; new server tools follow their group) and
  readOnlyOnly (only readOnlyHint=true tools), re-registered live.
- Settings card: per-group expandable tool rows with switches and a
  write marker; skills append a disabled-tools note.
- Toggles only control model visibility; netx enforces security.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
oliver 2026-10-16 22:08:22 +08:00
parent 2def53e7c7
commit 2ef3a43c63
38 changed files with 5049 additions and 17029 deletions

116
test/mcp-test-catalog.mjs Normal file
View file

@ -0,0 +1,116 @@
/**
* Seed the process-local MCP catalog for registration / publication tests.
*/
import { publishNetxConnection } from '../src/netx/runtime.ts'
const STORE_KEY = Symbol.for('dsh-netxops.mcp-catalog-store')
/** Classic ops catalog size (matches historical TOOLS_BY_GROUP.ops.length). */
export const OPS_TOOL_NAMES = Object.freeze([
'queryNmsAlarms',
'aggregateNmsAlarms',
'runNmsDiagnostics',
'queryNmsNeInventory',
'getNmsNe',
'queryNmsAlarmsRaw',
'aggregateNmsAlarmsRaw',
'listNmsAlarmFields',
'sqlQueryNms',
'listManagedNe',
'getManagedNe',
'execManagedNe',
'getNeExecJob',
'listCliTargets',
'findTopologyPaths',
])
export const TOPOLOGY_TOOL_NAMES = Object.freeze([
'getTopologyTree',
'getTopologyView',
'createTopologyFolder',
'addTopologyViewNodes',
'removeTopologyViewNodes',
'copyTopologyViewNodes',
'updateTopologyViewPositions',
'projectTopologyNeighbors',
'queryTopologyFabricNodes',
'classifyTopologyFabricNodes',
'queryTopologyNeighborhood',
'queryTopologyEdges',
'layoutTopologyView',
'suggestSinkHubs',
'analyzeTopologyViewLayout',
])
export const BIZ_MONITOR_TOOL_NAMES = Object.freeze([
'listBizMonitors',
'listBizMonitorBatches',
'getBizMonitorContext',
'getBizMonitorBoard',
'listBizMonitorReds',
'getBizMonitorDiffs',
'getBizCollectBatch',
'getBizCollectCommandRaw',
])
const WRITE_OPS = new Set(['execManagedNe'])
const WRITE_TOPOLOGY = new Set([
'createTopologyFolder',
'addTopologyViewNodes',
'removeTopologyViewNodes',
'copyTopologyViewNodes',
'updateTopologyViewPositions',
'projectTopologyNeighbors',
'classifyTopologyFabricNodes',
'layoutTopologyView',
])
function descriptor(name, group) {
const write = (group === 'ops' && WRITE_OPS.has(name))
|| (group === 'topology' && WRITE_TOPOLOGY.has(name))
return {
name,
description: name,
inputSchema: { type: 'object', properties: {} },
readOnlyHint: !write,
group,
}
}
export function fullCatalog() {
return [
...OPS_TOOL_NAMES.map((name) => descriptor(name, 'ops')),
...TOPOLOGY_TOOL_NAMES.map((name) => descriptor(name, 'topology')),
...BIZ_MONITOR_TOOL_NAMES.map((name) => descriptor(name, 'bizMonitor')),
]
}
/** Install a frozen catalog into the shared Symbol.for store (no network). */
export function seedMcpCatalog(tools = fullCatalog()) {
const root = globalThis
const store = {
state: {
phase: 'connected',
mcpUrl: 'http://127.0.0.1:8890/mcp',
tools: Object.freeze([...tools]),
error: '',
listedAt: new Date().toISOString(),
fromCache: false,
},
listeners: new Set(),
generation: 1,
}
root[STORE_KEY] = store
return store.state
}
export function clearMcpCatalogSeed() {
delete globalThis[STORE_KEY]
}
/** Convenience: seed catalog + publish a connection snapshot. */
export function publishWithCatalog(connection) {
seedMcpCatalog()
publishNetxConnection(connection)
}