diff --git a/.release-build.json b/.release-build.json index 6a7bc5e..400bb14 100644 --- a/.release-build.json +++ b/.release-build.json @@ -2,7 +2,7 @@ "format": 1, "builds": { "lib/agent-tools-biz-monitor.js": { - "output": "2b382bb050cd2bdbba42c018359fcd81fd56f7caa77ab5fcea7dded47841eeb5", + "output": "35b27ce03d1f8fe8b8bf919d1649d0506fd957dbcb1d7856c3991a2781559f11", "inputs": { "package.json": "1bb1fb8d615f49c69665a3a89b5912fa414a3b93aa3c541ce909ff57f97e100d", "pnpm-lock.yaml": "f1209ac70e9c2294e0f300c0aad88b64aeecd6aa19dfc929c5766c7d033615a3", @@ -25,15 +25,16 @@ "src/netx/kb-runtime.ts": "ebf2fd36c407439f1a59344af18d9fe7275825925adbe08719595d06161d6d5b", "src/netx/mcp-catalog.ts": "b79962da8f74f04595ed89b166b851d83cac41e8338b80b7629da2021a2c281d", "src/netx/mcp-remote.ts": "d79925538e6adaccd8dbf8c65a90027d98464d37a57dce00b3e3155c08b02843", + "src/netx/mcp-schema.ts": "2bb2eccd001101bd97389578c627931e89fbab4e057dab500e8b7b8bd655366f", "src/netx/mcp-store.ts": "eb93e466e16c3bca81bff01407689025711be52d55e79acbdfa4bec64bd0a6f3", "src/netx/mcp-types.ts": "4b30d6e8fba328b0e116c43a1a90d2b686ef4b9fc648b521063876426e9c2833", "src/netx/runtime.ts": "238555c92c3972ee9de8ec062b14d6a86961ec4c64609565ba1164b1a460d386", "src/netx/tool-mount-status.ts": "783981774d717ca5e7bf7c0ec7273b40787ef12a6b40749c74543b30fa390faf", - "src/netx/tools.ts": "0dfc3fdada56ab04ce40fecbe6f229aabae4ff6132d60809a8fb8daade948641" + "src/netx/tools.ts": "88c925aba50a7d6b3fc800986789955fd77c7aca8387c399b1ef58edbf7b9139" } }, "lib/agent-tools-common.js": { - "output": "3e51e500080dfad4c0a2ea6060afb1b7fe4e6a349d13030aed7ad37db49eedfa", + "output": "529aa5d79d0c40030920a8b8053797f43812bf6d8fa4cb6df7ea7e62a46e4777", "inputs": { "package.json": "1bb1fb8d615f49c69665a3a89b5912fa414a3b93aa3c541ce909ff57f97e100d", "pnpm-lock.yaml": "f1209ac70e9c2294e0f300c0aad88b64aeecd6aa19dfc929c5766c7d033615a3", @@ -56,15 +57,16 @@ "src/netx/kb-runtime.ts": "ebf2fd36c407439f1a59344af18d9fe7275825925adbe08719595d06161d6d5b", "src/netx/mcp-catalog.ts": "b79962da8f74f04595ed89b166b851d83cac41e8338b80b7629da2021a2c281d", "src/netx/mcp-remote.ts": "d79925538e6adaccd8dbf8c65a90027d98464d37a57dce00b3e3155c08b02843", + "src/netx/mcp-schema.ts": "2bb2eccd001101bd97389578c627931e89fbab4e057dab500e8b7b8bd655366f", "src/netx/mcp-store.ts": "eb93e466e16c3bca81bff01407689025711be52d55e79acbdfa4bec64bd0a6f3", "src/netx/mcp-types.ts": "4b30d6e8fba328b0e116c43a1a90d2b686ef4b9fc648b521063876426e9c2833", "src/netx/runtime.ts": "238555c92c3972ee9de8ec062b14d6a86961ec4c64609565ba1164b1a460d386", "src/netx/tool-mount-status.ts": "783981774d717ca5e7bf7c0ec7273b40787ef12a6b40749c74543b30fa390faf", - "src/netx/tools.ts": "0dfc3fdada56ab04ce40fecbe6f229aabae4ff6132d60809a8fb8daade948641" + "src/netx/tools.ts": "88c925aba50a7d6b3fc800986789955fd77c7aca8387c399b1ef58edbf7b9139" } }, "lib/agent-tools-nms.js": { - "output": "895e3fdf1ab1a9fc9a0fd0d79f2d51b22f164a1a6ea1e0897f975b4b3103d647", + "output": "75b7a381afdae9932b5d2ec9d4ca6fc63aa0e6da1c6ff1466c57759e6a932528", "inputs": { "package.json": "1bb1fb8d615f49c69665a3a89b5912fa414a3b93aa3c541ce909ff57f97e100d", "pnpm-lock.yaml": "f1209ac70e9c2294e0f300c0aad88b64aeecd6aa19dfc929c5766c7d033615a3", @@ -87,15 +89,16 @@ "src/netx/kb-runtime.ts": "ebf2fd36c407439f1a59344af18d9fe7275825925adbe08719595d06161d6d5b", "src/netx/mcp-catalog.ts": "b79962da8f74f04595ed89b166b851d83cac41e8338b80b7629da2021a2c281d", "src/netx/mcp-remote.ts": "d79925538e6adaccd8dbf8c65a90027d98464d37a57dce00b3e3155c08b02843", + "src/netx/mcp-schema.ts": "2bb2eccd001101bd97389578c627931e89fbab4e057dab500e8b7b8bd655366f", "src/netx/mcp-store.ts": "eb93e466e16c3bca81bff01407689025711be52d55e79acbdfa4bec64bd0a6f3", "src/netx/mcp-types.ts": "4b30d6e8fba328b0e116c43a1a90d2b686ef4b9fc648b521063876426e9c2833", "src/netx/runtime.ts": "238555c92c3972ee9de8ec062b14d6a86961ec4c64609565ba1164b1a460d386", "src/netx/tool-mount-status.ts": "783981774d717ca5e7bf7c0ec7273b40787ef12a6b40749c74543b30fa390faf", - "src/netx/tools.ts": "0dfc3fdada56ab04ce40fecbe6f229aabae4ff6132d60809a8fb8daade948641" + "src/netx/tools.ts": "88c925aba50a7d6b3fc800986789955fd77c7aca8387c399b1ef58edbf7b9139" } }, "lib/agent-tools-ops.js": { - "output": "4ece4e983ee4c50a0591a9e601a6cded04f55ebe1c849b4b2eb9d458854e0a18", + "output": "16151108566b1f3313bfaace216ebb84656704c236b90cb468ba7f559dcd5e96", "inputs": { "package.json": "1bb1fb8d615f49c69665a3a89b5912fa414a3b93aa3c541ce909ff57f97e100d", "pnpm-lock.yaml": "f1209ac70e9c2294e0f300c0aad88b64aeecd6aa19dfc929c5766c7d033615a3", @@ -118,15 +121,16 @@ "src/netx/kb-runtime.ts": "ebf2fd36c407439f1a59344af18d9fe7275825925adbe08719595d06161d6d5b", "src/netx/mcp-catalog.ts": "b79962da8f74f04595ed89b166b851d83cac41e8338b80b7629da2021a2c281d", "src/netx/mcp-remote.ts": "d79925538e6adaccd8dbf8c65a90027d98464d37a57dce00b3e3155c08b02843", + "src/netx/mcp-schema.ts": "2bb2eccd001101bd97389578c627931e89fbab4e057dab500e8b7b8bd655366f", "src/netx/mcp-store.ts": "eb93e466e16c3bca81bff01407689025711be52d55e79acbdfa4bec64bd0a6f3", "src/netx/mcp-types.ts": "4b30d6e8fba328b0e116c43a1a90d2b686ef4b9fc648b521063876426e9c2833", "src/netx/runtime.ts": "238555c92c3972ee9de8ec062b14d6a86961ec4c64609565ba1164b1a460d386", "src/netx/tool-mount-status.ts": "783981774d717ca5e7bf7c0ec7273b40787ef12a6b40749c74543b30fa390faf", - "src/netx/tools.ts": "0dfc3fdada56ab04ce40fecbe6f229aabae4ff6132d60809a8fb8daade948641" + "src/netx/tools.ts": "88c925aba50a7d6b3fc800986789955fd77c7aca8387c399b1ef58edbf7b9139" } }, "lib/agent-tools-topology.js": { - "output": "cec2f877d8fe03b5e537c35ec39a0d66c5885910385f690a8a3bcb96e8ad3954", + "output": "2e3b6268a7e11cab0717faaf4d13bc3ee5a195f1aea94960118211f6773dac28", "inputs": { "package.json": "1bb1fb8d615f49c69665a3a89b5912fa414a3b93aa3c541ce909ff57f97e100d", "pnpm-lock.yaml": "f1209ac70e9c2294e0f300c0aad88b64aeecd6aa19dfc929c5766c7d033615a3", @@ -149,15 +153,16 @@ "src/netx/kb-runtime.ts": "ebf2fd36c407439f1a59344af18d9fe7275825925adbe08719595d06161d6d5b", "src/netx/mcp-catalog.ts": "b79962da8f74f04595ed89b166b851d83cac41e8338b80b7629da2021a2c281d", "src/netx/mcp-remote.ts": "d79925538e6adaccd8dbf8c65a90027d98464d37a57dce00b3e3155c08b02843", + "src/netx/mcp-schema.ts": "2bb2eccd001101bd97389578c627931e89fbab4e057dab500e8b7b8bd655366f", "src/netx/mcp-store.ts": "eb93e466e16c3bca81bff01407689025711be52d55e79acbdfa4bec64bd0a6f3", "src/netx/mcp-types.ts": "4b30d6e8fba328b0e116c43a1a90d2b686ef4b9fc648b521063876426e9c2833", "src/netx/runtime.ts": "238555c92c3972ee9de8ec062b14d6a86961ec4c64609565ba1164b1a460d386", "src/netx/tool-mount-status.ts": "783981774d717ca5e7bf7c0ec7273b40787ef12a6b40749c74543b30fa390faf", - "src/netx/tools.ts": "0dfc3fdada56ab04ce40fecbe6f229aabae4ff6132d60809a8fb8daade948641" + "src/netx/tools.ts": "88c925aba50a7d6b3fc800986789955fd77c7aca8387c399b1ef58edbf7b9139" } }, "lib/agent-tools.js": { - "output": "9c51c136d5b5be9de1479abcae7bd4a69a20fe4e1f92562ca6e13e59663669cc", + "output": "d14c5187ce4ea92c7a9e2a54949c6381c8ad9aede9a78264eeaca82d6618de6a", "inputs": { "package.json": "1bb1fb8d615f49c69665a3a89b5912fa414a3b93aa3c541ce909ff57f97e100d", "pnpm-lock.yaml": "f1209ac70e9c2294e0f300c0aad88b64aeecd6aa19dfc929c5766c7d033615a3", @@ -180,11 +185,12 @@ "src/netx/kb-runtime.ts": "ebf2fd36c407439f1a59344af18d9fe7275825925adbe08719595d06161d6d5b", "src/netx/mcp-catalog.ts": "b79962da8f74f04595ed89b166b851d83cac41e8338b80b7629da2021a2c281d", "src/netx/mcp-remote.ts": "d79925538e6adaccd8dbf8c65a90027d98464d37a57dce00b3e3155c08b02843", + "src/netx/mcp-schema.ts": "2bb2eccd001101bd97389578c627931e89fbab4e057dab500e8b7b8bd655366f", "src/netx/mcp-store.ts": "eb93e466e16c3bca81bff01407689025711be52d55e79acbdfa4bec64bd0a6f3", "src/netx/mcp-types.ts": "4b30d6e8fba328b0e116c43a1a90d2b686ef4b9fc648b521063876426e9c2833", "src/netx/runtime.ts": "238555c92c3972ee9de8ec062b14d6a86961ec4c64609565ba1164b1a460d386", "src/netx/tool-mount-status.ts": "783981774d717ca5e7bf7c0ec7273b40787ef12a6b40749c74543b30fa390faf", - "src/netx/tools.ts": "0dfc3fdada56ab04ce40fecbe6f229aabae4ff6132d60809a8fb8daade948641" + "src/netx/tools.ts": "88c925aba50a7d6b3fc800986789955fd77c7aca8387c399b1ef58edbf7b9139" } }, "lib/client.js": { @@ -219,7 +225,7 @@ } }, "lib/index.js": { - "output": "778c85d88498127abf5bb490b251b8b6f94b3ec38a8d7ad5c46c13854cc65d1f", + "output": "ceb3cb1b0e366d086fdca112904a73d8dbb0c9926064bbe13e8602e86267307d", "inputs": { "package.json": "1bb1fb8d615f49c69665a3a89b5912fa414a3b93aa3c541ce909ff57f97e100d", "pnpm-lock.yaml": "f1209ac70e9c2294e0f300c0aad88b64aeecd6aa19dfc929c5766c7d033615a3", @@ -251,6 +257,7 @@ "src/netx/kb-runtime.ts": "ebf2fd36c407439f1a59344af18d9fe7275825925adbe08719595d06161d6d5b", "src/netx/mcp-catalog.ts": "b79962da8f74f04595ed89b166b851d83cac41e8338b80b7629da2021a2c281d", "src/netx/mcp-remote.ts": "d79925538e6adaccd8dbf8c65a90027d98464d37a57dce00b3e3155c08b02843", + "src/netx/mcp-schema.ts": "2bb2eccd001101bd97389578c627931e89fbab4e057dab500e8b7b8bd655366f", "src/netx/mcp-store.ts": "eb93e466e16c3bca81bff01407689025711be52d55e79acbdfa4bec64bd0a6f3", "src/netx/mcp-types.ts": "4b30d6e8fba328b0e116c43a1a90d2b686ef4b9fc648b521063876426e9c2833", "src/netx/model-language.ts": "07df448b407086237c184a71bfc78e7cb27e50d7f8c499995d8b368b14780c9e", @@ -259,7 +266,7 @@ "src/netx/runtime.ts": "238555c92c3972ee9de8ec062b14d6a86961ec4c64609565ba1164b1a460d386", "src/netx/session-export.ts": "8e6e9a3094045901563366fac3d6dc74d9bfd78bbff853358d6500955d3dcff6", "src/netx/tool-mount-status.ts": "783981774d717ca5e7bf7c0ec7273b40787ef12a6b40749c74543b30fa390faf", - "src/netx/tools.ts": "0dfc3fdada56ab04ce40fecbe6f229aabae4ff6132d60809a8fb8daade948641", + "src/netx/tools.ts": "88c925aba50a7d6b3fc800986789955fd77c7aca8387c399b1ef58edbf7b9139", "src/session-export-shared.ts": "e30d884ec22a0ea2e016c1479f62a8bc8d7ddcce5369a36647d411d07a8620d5" } } diff --git a/lib/agent-tools-biz-monitor.js b/lib/agent-tools-biz-monitor.js index 711f9cb..9368410 100644 --- a/lib/agent-tools-biz-monitor.js +++ b/lib/agent-tools-biz-monitor.js @@ -1803,6 +1803,104 @@ import { readFileSync as readFileSync5 } from "node:fs"; import { dirname as dirname5, join as join9 } from "node:path"; import { fileURLToPath as fileURLToPath2 } from "node:url"; import { defineTool as defineTool2 } from "@deepseek-ai/dsh-tools"; + +// src/netx/mcp-schema.ts +var ANNOTATIONS = ["description", "title", "default", "examples"]; +var SCALAR_TYPES = new Set(["string", "number", "integer", "boolean", "null"]); +var CONSTRAINTS = [ + "minimum", + "maximum", + "exclusiveMinimum", + "exclusiveMaximum", + "multipleOf", + "minLength", + "maxLength", + "pattern", + "format", + "minItems", + "maxItems", + "uniqueItems" +]; +function isRecord(value) { + return typeof value === "object" && value !== null && !Array.isArray(value); +} +function withAnnotations(source, target) { + for (const key of ANNOTATIONS) { + if (Object.hasOwn(source, key)) + target[key] = source[key]; + } + const notes = CONSTRAINTS.filter((key) => Object.hasOwn(source, key)).map((key) => `${key}=${JSON.stringify(source[key])}`); + if (notes.length > 0) { + const base = typeof target.description === "string" ? target.description : ""; + target.description = `${base}${base ? " " : ""}(${notes.join(", ")})`; + } + return target; +} +function variants(node) { + const list = Array.isArray(node.oneOf) ? node.oneOf : Array.isArray(node.anyOf) ? node.anyOf : undefined; + if (list) + return list.filter(isRecord).filter((branch) => branch.type !== "null"); + if (Array.isArray(node.type)) { + const { type: _types, ...rest } = node; + return node.type.filter((t) => t !== "null").map((t) => ({ ...rest, type: t })); + } + return; +} +function toValue(node) { + if (!isRecord(node)) + return { type: "json" }; + const branches = variants(node); + if (branches) { + if (branches.length === 0) + return withAnnotations(node, { type: "json" }); + if (branches.length === 1) + return withAnnotations(node, toValue({ ...branches[0] })); + return withAnnotations(node, { oneOf: branches.map(toValue) }); + } + const type = node.type; + if (type === "object" || type === undefined && isRecord(node.properties)) { + const out = { + type: "object", + additionalProperties: typeof node.additionalProperties === "boolean" ? node.additionalProperties : true + }; + if (isRecord(node.properties)) + out.properties = toProperties(node.properties, node.required); + return withAnnotations(node, out); + } + if (type === "array") { + const out = { type: "array" }; + if (Object.hasOwn(node, "items")) + out.items = toValue(node.items); + return withAnnotations(node, out); + } + if (typeof type === "string" && SCALAR_TYPES.has(type)) { + const out = { type }; + if (Array.isArray(node.enum) && node.enum.length > 0) + out.enum = node.enum; + if (Object.hasOwn(node, "const")) + out.const = node.const; + return withAnnotations(node, out); + } + return withAnnotations(node, { type: "json" }); +} +function toProperties(properties, required) { + const requiredSet = new Set(Array.isArray(required) ? required.filter((k) => typeof k === "string") : []); + const out = {}; + for (const [key, value] of Object.entries(properties)) { + const converted = toValue(value); + if (requiredSet.has(key)) + converted.required = true; + out[key] = converted; + } + return out; +} +function parametersFromInputSchema(schema) { + if (!isRecord(schema) || !isRecord(schema.properties)) + return {}; + return toProperties(schema.properties, schema.required); +} + +// src/netx/tools.ts function packageVersion() { try { const here = dirname5(fileURLToPath2(import.meta.url)); @@ -1828,18 +1926,6 @@ var mcpOut = { schema: { type: "object", additionalProperties: true }, render: renderMcpResult }; -function parametersFromInputSchema(schema) { - if (schema && typeof schema === "object" && !Array.isArray(schema)) { - const props = schema.properties; - if (props && typeof props === "object" && !Array.isArray(props)) { - return props; - } - if (schema.type === undefined && schema.properties === undefined) { - return schema; - } - } - return {}; -} function defineRemoteTool(tool2, timeoutMs) { const name = publicToolName(tool2.name); return defineTool2({ diff --git a/lib/agent-tools-common.js b/lib/agent-tools-common.js index 4b147e9..e48e445 100644 --- a/lib/agent-tools-common.js +++ b/lib/agent-tools-common.js @@ -1803,6 +1803,104 @@ import { readFileSync as readFileSync5 } from "node:fs"; import { dirname as dirname5, join as join9 } from "node:path"; import { fileURLToPath as fileURLToPath2 } from "node:url"; import { defineTool as defineTool2 } from "@deepseek-ai/dsh-tools"; + +// src/netx/mcp-schema.ts +var ANNOTATIONS = ["description", "title", "default", "examples"]; +var SCALAR_TYPES = new Set(["string", "number", "integer", "boolean", "null"]); +var CONSTRAINTS = [ + "minimum", + "maximum", + "exclusiveMinimum", + "exclusiveMaximum", + "multipleOf", + "minLength", + "maxLength", + "pattern", + "format", + "minItems", + "maxItems", + "uniqueItems" +]; +function isRecord(value) { + return typeof value === "object" && value !== null && !Array.isArray(value); +} +function withAnnotations(source, target) { + for (const key of ANNOTATIONS) { + if (Object.hasOwn(source, key)) + target[key] = source[key]; + } + const notes = CONSTRAINTS.filter((key) => Object.hasOwn(source, key)).map((key) => `${key}=${JSON.stringify(source[key])}`); + if (notes.length > 0) { + const base = typeof target.description === "string" ? target.description : ""; + target.description = `${base}${base ? " " : ""}(${notes.join(", ")})`; + } + return target; +} +function variants(node) { + const list = Array.isArray(node.oneOf) ? node.oneOf : Array.isArray(node.anyOf) ? node.anyOf : undefined; + if (list) + return list.filter(isRecord).filter((branch) => branch.type !== "null"); + if (Array.isArray(node.type)) { + const { type: _types, ...rest } = node; + return node.type.filter((t) => t !== "null").map((t) => ({ ...rest, type: t })); + } + return; +} +function toValue(node) { + if (!isRecord(node)) + return { type: "json" }; + const branches = variants(node); + if (branches) { + if (branches.length === 0) + return withAnnotations(node, { type: "json" }); + if (branches.length === 1) + return withAnnotations(node, toValue({ ...branches[0] })); + return withAnnotations(node, { oneOf: branches.map(toValue) }); + } + const type = node.type; + if (type === "object" || type === undefined && isRecord(node.properties)) { + const out = { + type: "object", + additionalProperties: typeof node.additionalProperties === "boolean" ? node.additionalProperties : true + }; + if (isRecord(node.properties)) + out.properties = toProperties(node.properties, node.required); + return withAnnotations(node, out); + } + if (type === "array") { + const out = { type: "array" }; + if (Object.hasOwn(node, "items")) + out.items = toValue(node.items); + return withAnnotations(node, out); + } + if (typeof type === "string" && SCALAR_TYPES.has(type)) { + const out = { type }; + if (Array.isArray(node.enum) && node.enum.length > 0) + out.enum = node.enum; + if (Object.hasOwn(node, "const")) + out.const = node.const; + return withAnnotations(node, out); + } + return withAnnotations(node, { type: "json" }); +} +function toProperties(properties, required) { + const requiredSet = new Set(Array.isArray(required) ? required.filter((k) => typeof k === "string") : []); + const out = {}; + for (const [key, value] of Object.entries(properties)) { + const converted = toValue(value); + if (requiredSet.has(key)) + converted.required = true; + out[key] = converted; + } + return out; +} +function parametersFromInputSchema(schema) { + if (!isRecord(schema) || !isRecord(schema.properties)) + return {}; + return toProperties(schema.properties, schema.required); +} + +// src/netx/tools.ts function packageVersion() { try { const here = dirname5(fileURLToPath2(import.meta.url)); @@ -1828,18 +1926,6 @@ var mcpOut = { schema: { type: "object", additionalProperties: true }, render: renderMcpResult }; -function parametersFromInputSchema(schema) { - if (schema && typeof schema === "object" && !Array.isArray(schema)) { - const props = schema.properties; - if (props && typeof props === "object" && !Array.isArray(props)) { - return props; - } - if (schema.type === undefined && schema.properties === undefined) { - return schema; - } - } - return {}; -} function defineRemoteTool(tool2, timeoutMs) { const name = publicToolName(tool2.name); return defineTool2({ diff --git a/lib/agent-tools-nms.js b/lib/agent-tools-nms.js index 7d62d2e..938f600 100644 --- a/lib/agent-tools-nms.js +++ b/lib/agent-tools-nms.js @@ -1803,6 +1803,104 @@ import { readFileSync as readFileSync5 } from "node:fs"; import { dirname as dirname5, join as join9 } from "node:path"; import { fileURLToPath as fileURLToPath2 } from "node:url"; import { defineTool as defineTool2 } from "@deepseek-ai/dsh-tools"; + +// src/netx/mcp-schema.ts +var ANNOTATIONS = ["description", "title", "default", "examples"]; +var SCALAR_TYPES = new Set(["string", "number", "integer", "boolean", "null"]); +var CONSTRAINTS = [ + "minimum", + "maximum", + "exclusiveMinimum", + "exclusiveMaximum", + "multipleOf", + "minLength", + "maxLength", + "pattern", + "format", + "minItems", + "maxItems", + "uniqueItems" +]; +function isRecord(value) { + return typeof value === "object" && value !== null && !Array.isArray(value); +} +function withAnnotations(source, target) { + for (const key of ANNOTATIONS) { + if (Object.hasOwn(source, key)) + target[key] = source[key]; + } + const notes = CONSTRAINTS.filter((key) => Object.hasOwn(source, key)).map((key) => `${key}=${JSON.stringify(source[key])}`); + if (notes.length > 0) { + const base = typeof target.description === "string" ? target.description : ""; + target.description = `${base}${base ? " " : ""}(${notes.join(", ")})`; + } + return target; +} +function variants(node) { + const list = Array.isArray(node.oneOf) ? node.oneOf : Array.isArray(node.anyOf) ? node.anyOf : undefined; + if (list) + return list.filter(isRecord).filter((branch) => branch.type !== "null"); + if (Array.isArray(node.type)) { + const { type: _types, ...rest } = node; + return node.type.filter((t) => t !== "null").map((t) => ({ ...rest, type: t })); + } + return; +} +function toValue(node) { + if (!isRecord(node)) + return { type: "json" }; + const branches = variants(node); + if (branches) { + if (branches.length === 0) + return withAnnotations(node, { type: "json" }); + if (branches.length === 1) + return withAnnotations(node, toValue({ ...branches[0] })); + return withAnnotations(node, { oneOf: branches.map(toValue) }); + } + const type = node.type; + if (type === "object" || type === undefined && isRecord(node.properties)) { + const out = { + type: "object", + additionalProperties: typeof node.additionalProperties === "boolean" ? node.additionalProperties : true + }; + if (isRecord(node.properties)) + out.properties = toProperties(node.properties, node.required); + return withAnnotations(node, out); + } + if (type === "array") { + const out = { type: "array" }; + if (Object.hasOwn(node, "items")) + out.items = toValue(node.items); + return withAnnotations(node, out); + } + if (typeof type === "string" && SCALAR_TYPES.has(type)) { + const out = { type }; + if (Array.isArray(node.enum) && node.enum.length > 0) + out.enum = node.enum; + if (Object.hasOwn(node, "const")) + out.const = node.const; + return withAnnotations(node, out); + } + return withAnnotations(node, { type: "json" }); +} +function toProperties(properties, required) { + const requiredSet = new Set(Array.isArray(required) ? required.filter((k) => typeof k === "string") : []); + const out = {}; + for (const [key, value] of Object.entries(properties)) { + const converted = toValue(value); + if (requiredSet.has(key)) + converted.required = true; + out[key] = converted; + } + return out; +} +function parametersFromInputSchema(schema) { + if (!isRecord(schema) || !isRecord(schema.properties)) + return {}; + return toProperties(schema.properties, schema.required); +} + +// src/netx/tools.ts function packageVersion() { try { const here = dirname5(fileURLToPath2(import.meta.url)); @@ -1828,18 +1926,6 @@ var mcpOut = { schema: { type: "object", additionalProperties: true }, render: renderMcpResult }; -function parametersFromInputSchema(schema) { - if (schema && typeof schema === "object" && !Array.isArray(schema)) { - const props = schema.properties; - if (props && typeof props === "object" && !Array.isArray(props)) { - return props; - } - if (schema.type === undefined && schema.properties === undefined) { - return schema; - } - } - return {}; -} function defineRemoteTool(tool2, timeoutMs) { const name = publicToolName(tool2.name); return defineTool2({ diff --git a/lib/agent-tools-ops.js b/lib/agent-tools-ops.js index 6bdd43f..52708ba 100644 --- a/lib/agent-tools-ops.js +++ b/lib/agent-tools-ops.js @@ -1803,6 +1803,104 @@ import { readFileSync as readFileSync5 } from "node:fs"; import { dirname as dirname5, join as join9 } from "node:path"; import { fileURLToPath as fileURLToPath2 } from "node:url"; import { defineTool as defineTool2 } from "@deepseek-ai/dsh-tools"; + +// src/netx/mcp-schema.ts +var ANNOTATIONS = ["description", "title", "default", "examples"]; +var SCALAR_TYPES = new Set(["string", "number", "integer", "boolean", "null"]); +var CONSTRAINTS = [ + "minimum", + "maximum", + "exclusiveMinimum", + "exclusiveMaximum", + "multipleOf", + "minLength", + "maxLength", + "pattern", + "format", + "minItems", + "maxItems", + "uniqueItems" +]; +function isRecord(value) { + return typeof value === "object" && value !== null && !Array.isArray(value); +} +function withAnnotations(source, target) { + for (const key of ANNOTATIONS) { + if (Object.hasOwn(source, key)) + target[key] = source[key]; + } + const notes = CONSTRAINTS.filter((key) => Object.hasOwn(source, key)).map((key) => `${key}=${JSON.stringify(source[key])}`); + if (notes.length > 0) { + const base = typeof target.description === "string" ? target.description : ""; + target.description = `${base}${base ? " " : ""}(${notes.join(", ")})`; + } + return target; +} +function variants(node) { + const list = Array.isArray(node.oneOf) ? node.oneOf : Array.isArray(node.anyOf) ? node.anyOf : undefined; + if (list) + return list.filter(isRecord).filter((branch) => branch.type !== "null"); + if (Array.isArray(node.type)) { + const { type: _types, ...rest } = node; + return node.type.filter((t) => t !== "null").map((t) => ({ ...rest, type: t })); + } + return; +} +function toValue(node) { + if (!isRecord(node)) + return { type: "json" }; + const branches = variants(node); + if (branches) { + if (branches.length === 0) + return withAnnotations(node, { type: "json" }); + if (branches.length === 1) + return withAnnotations(node, toValue({ ...branches[0] })); + return withAnnotations(node, { oneOf: branches.map(toValue) }); + } + const type = node.type; + if (type === "object" || type === undefined && isRecord(node.properties)) { + const out = { + type: "object", + additionalProperties: typeof node.additionalProperties === "boolean" ? node.additionalProperties : true + }; + if (isRecord(node.properties)) + out.properties = toProperties(node.properties, node.required); + return withAnnotations(node, out); + } + if (type === "array") { + const out = { type: "array" }; + if (Object.hasOwn(node, "items")) + out.items = toValue(node.items); + return withAnnotations(node, out); + } + if (typeof type === "string" && SCALAR_TYPES.has(type)) { + const out = { type }; + if (Array.isArray(node.enum) && node.enum.length > 0) + out.enum = node.enum; + if (Object.hasOwn(node, "const")) + out.const = node.const; + return withAnnotations(node, out); + } + return withAnnotations(node, { type: "json" }); +} +function toProperties(properties, required) { + const requiredSet = new Set(Array.isArray(required) ? required.filter((k) => typeof k === "string") : []); + const out = {}; + for (const [key, value] of Object.entries(properties)) { + const converted = toValue(value); + if (requiredSet.has(key)) + converted.required = true; + out[key] = converted; + } + return out; +} +function parametersFromInputSchema(schema) { + if (!isRecord(schema) || !isRecord(schema.properties)) + return {}; + return toProperties(schema.properties, schema.required); +} + +// src/netx/tools.ts function packageVersion() { try { const here = dirname5(fileURLToPath2(import.meta.url)); @@ -1828,18 +1926,6 @@ var mcpOut = { schema: { type: "object", additionalProperties: true }, render: renderMcpResult }; -function parametersFromInputSchema(schema) { - if (schema && typeof schema === "object" && !Array.isArray(schema)) { - const props = schema.properties; - if (props && typeof props === "object" && !Array.isArray(props)) { - return props; - } - if (schema.type === undefined && schema.properties === undefined) { - return schema; - } - } - return {}; -} function defineRemoteTool(tool2, timeoutMs) { const name = publicToolName(tool2.name); return defineTool2({ diff --git a/lib/agent-tools-topology.js b/lib/agent-tools-topology.js index f7e5007..fa186a4 100644 --- a/lib/agent-tools-topology.js +++ b/lib/agent-tools-topology.js @@ -1803,6 +1803,104 @@ import { readFileSync as readFileSync5 } from "node:fs"; import { dirname as dirname5, join as join9 } from "node:path"; import { fileURLToPath as fileURLToPath2 } from "node:url"; import { defineTool as defineTool2 } from "@deepseek-ai/dsh-tools"; + +// src/netx/mcp-schema.ts +var ANNOTATIONS = ["description", "title", "default", "examples"]; +var SCALAR_TYPES = new Set(["string", "number", "integer", "boolean", "null"]); +var CONSTRAINTS = [ + "minimum", + "maximum", + "exclusiveMinimum", + "exclusiveMaximum", + "multipleOf", + "minLength", + "maxLength", + "pattern", + "format", + "minItems", + "maxItems", + "uniqueItems" +]; +function isRecord(value) { + return typeof value === "object" && value !== null && !Array.isArray(value); +} +function withAnnotations(source, target) { + for (const key of ANNOTATIONS) { + if (Object.hasOwn(source, key)) + target[key] = source[key]; + } + const notes = CONSTRAINTS.filter((key) => Object.hasOwn(source, key)).map((key) => `${key}=${JSON.stringify(source[key])}`); + if (notes.length > 0) { + const base = typeof target.description === "string" ? target.description : ""; + target.description = `${base}${base ? " " : ""}(${notes.join(", ")})`; + } + return target; +} +function variants(node) { + const list = Array.isArray(node.oneOf) ? node.oneOf : Array.isArray(node.anyOf) ? node.anyOf : undefined; + if (list) + return list.filter(isRecord).filter((branch) => branch.type !== "null"); + if (Array.isArray(node.type)) { + const { type: _types, ...rest } = node; + return node.type.filter((t) => t !== "null").map((t) => ({ ...rest, type: t })); + } + return; +} +function toValue(node) { + if (!isRecord(node)) + return { type: "json" }; + const branches = variants(node); + if (branches) { + if (branches.length === 0) + return withAnnotations(node, { type: "json" }); + if (branches.length === 1) + return withAnnotations(node, toValue({ ...branches[0] })); + return withAnnotations(node, { oneOf: branches.map(toValue) }); + } + const type = node.type; + if (type === "object" || type === undefined && isRecord(node.properties)) { + const out = { + type: "object", + additionalProperties: typeof node.additionalProperties === "boolean" ? node.additionalProperties : true + }; + if (isRecord(node.properties)) + out.properties = toProperties(node.properties, node.required); + return withAnnotations(node, out); + } + if (type === "array") { + const out = { type: "array" }; + if (Object.hasOwn(node, "items")) + out.items = toValue(node.items); + return withAnnotations(node, out); + } + if (typeof type === "string" && SCALAR_TYPES.has(type)) { + const out = { type }; + if (Array.isArray(node.enum) && node.enum.length > 0) + out.enum = node.enum; + if (Object.hasOwn(node, "const")) + out.const = node.const; + return withAnnotations(node, out); + } + return withAnnotations(node, { type: "json" }); +} +function toProperties(properties, required) { + const requiredSet = new Set(Array.isArray(required) ? required.filter((k) => typeof k === "string") : []); + const out = {}; + for (const [key, value] of Object.entries(properties)) { + const converted = toValue(value); + if (requiredSet.has(key)) + converted.required = true; + out[key] = converted; + } + return out; +} +function parametersFromInputSchema(schema) { + if (!isRecord(schema) || !isRecord(schema.properties)) + return {}; + return toProperties(schema.properties, schema.required); +} + +// src/netx/tools.ts function packageVersion() { try { const here = dirname5(fileURLToPath2(import.meta.url)); @@ -1828,18 +1926,6 @@ var mcpOut = { schema: { type: "object", additionalProperties: true }, render: renderMcpResult }; -function parametersFromInputSchema(schema) { - if (schema && typeof schema === "object" && !Array.isArray(schema)) { - const props = schema.properties; - if (props && typeof props === "object" && !Array.isArray(props)) { - return props; - } - if (schema.type === undefined && schema.properties === undefined) { - return schema; - } - } - return {}; -} function defineRemoteTool(tool2, timeoutMs) { const name = publicToolName(tool2.name); return defineTool2({ diff --git a/lib/agent-tools.js b/lib/agent-tools.js index a4add75..a7a874a 100644 --- a/lib/agent-tools.js +++ b/lib/agent-tools.js @@ -1803,6 +1803,104 @@ import { readFileSync as readFileSync5 } from "node:fs"; import { dirname as dirname5, join as join9 } from "node:path"; import { fileURLToPath as fileURLToPath2 } from "node:url"; import { defineTool as defineTool2 } from "@deepseek-ai/dsh-tools"; + +// src/netx/mcp-schema.ts +var ANNOTATIONS = ["description", "title", "default", "examples"]; +var SCALAR_TYPES = new Set(["string", "number", "integer", "boolean", "null"]); +var CONSTRAINTS = [ + "minimum", + "maximum", + "exclusiveMinimum", + "exclusiveMaximum", + "multipleOf", + "minLength", + "maxLength", + "pattern", + "format", + "minItems", + "maxItems", + "uniqueItems" +]; +function isRecord(value) { + return typeof value === "object" && value !== null && !Array.isArray(value); +} +function withAnnotations(source, target) { + for (const key of ANNOTATIONS) { + if (Object.hasOwn(source, key)) + target[key] = source[key]; + } + const notes = CONSTRAINTS.filter((key) => Object.hasOwn(source, key)).map((key) => `${key}=${JSON.stringify(source[key])}`); + if (notes.length > 0) { + const base = typeof target.description === "string" ? target.description : ""; + target.description = `${base}${base ? " " : ""}(${notes.join(", ")})`; + } + return target; +} +function variants(node) { + const list = Array.isArray(node.oneOf) ? node.oneOf : Array.isArray(node.anyOf) ? node.anyOf : undefined; + if (list) + return list.filter(isRecord).filter((branch) => branch.type !== "null"); + if (Array.isArray(node.type)) { + const { type: _types, ...rest } = node; + return node.type.filter((t) => t !== "null").map((t) => ({ ...rest, type: t })); + } + return; +} +function toValue(node) { + if (!isRecord(node)) + return { type: "json" }; + const branches = variants(node); + if (branches) { + if (branches.length === 0) + return withAnnotations(node, { type: "json" }); + if (branches.length === 1) + return withAnnotations(node, toValue({ ...branches[0] })); + return withAnnotations(node, { oneOf: branches.map(toValue) }); + } + const type = node.type; + if (type === "object" || type === undefined && isRecord(node.properties)) { + const out = { + type: "object", + additionalProperties: typeof node.additionalProperties === "boolean" ? node.additionalProperties : true + }; + if (isRecord(node.properties)) + out.properties = toProperties(node.properties, node.required); + return withAnnotations(node, out); + } + if (type === "array") { + const out = { type: "array" }; + if (Object.hasOwn(node, "items")) + out.items = toValue(node.items); + return withAnnotations(node, out); + } + if (typeof type === "string" && SCALAR_TYPES.has(type)) { + const out = { type }; + if (Array.isArray(node.enum) && node.enum.length > 0) + out.enum = node.enum; + if (Object.hasOwn(node, "const")) + out.const = node.const; + return withAnnotations(node, out); + } + return withAnnotations(node, { type: "json" }); +} +function toProperties(properties, required) { + const requiredSet = new Set(Array.isArray(required) ? required.filter((k) => typeof k === "string") : []); + const out = {}; + for (const [key, value] of Object.entries(properties)) { + const converted = toValue(value); + if (requiredSet.has(key)) + converted.required = true; + out[key] = converted; + } + return out; +} +function parametersFromInputSchema(schema) { + if (!isRecord(schema) || !isRecord(schema.properties)) + return {}; + return toProperties(schema.properties, schema.required); +} + +// src/netx/tools.ts function packageVersion() { try { const here = dirname5(fileURLToPath2(import.meta.url)); @@ -1828,18 +1926,6 @@ var mcpOut = { schema: { type: "object", additionalProperties: true }, render: renderMcpResult }; -function parametersFromInputSchema(schema) { - if (schema && typeof schema === "object" && !Array.isArray(schema)) { - const props = schema.properties; - if (props && typeof props === "object" && !Array.isArray(props)) { - return props; - } - if (schema.type === undefined && schema.properties === undefined) { - return schema; - } - } - return {}; -} function defineRemoteTool(tool2, timeoutMs) { const name = publicToolName(tool2.name); return defineTool2({ diff --git a/lib/index.js b/lib/index.js index df9648c..7255423 100644 --- a/lib/index.js +++ b/lib/index.js @@ -3217,7 +3217,7 @@ function watchNetxConnection(listener) { // src/netx/session-export.ts import { homedir as homedir4, hostname as osHostname } from "node:os"; -// ../../netxops/node_modules/.pnpm/fflate@0.8.3/node_modules/fflate/esm/index.mjs +// node_modules/.pnpm/fflate@0.8.3/node_modules/fflate/esm/index.mjs import { createRequire as createRequire3 } from "module"; var require2 = createRequire3("/"); var _a; @@ -4744,6 +4744,102 @@ function mcpContentText(content, fallback = "") { `) || fallback; } +// src/netx/mcp-schema.ts +var ANNOTATIONS = ["description", "title", "default", "examples"]; +var SCALAR_TYPES = new Set(["string", "number", "integer", "boolean", "null"]); +var CONSTRAINTS = [ + "minimum", + "maximum", + "exclusiveMinimum", + "exclusiveMaximum", + "multipleOf", + "minLength", + "maxLength", + "pattern", + "format", + "minItems", + "maxItems", + "uniqueItems" +]; +function isRecord(value) { + return typeof value === "object" && value !== null && !Array.isArray(value); +} +function withAnnotations(source, target) { + for (const key of ANNOTATIONS) { + if (Object.hasOwn(source, key)) + target[key] = source[key]; + } + const notes = CONSTRAINTS.filter((key) => Object.hasOwn(source, key)).map((key) => `${key}=${JSON.stringify(source[key])}`); + if (notes.length > 0) { + const base = typeof target.description === "string" ? target.description : ""; + target.description = `${base}${base ? " " : ""}(${notes.join(", ")})`; + } + return target; +} +function variants(node) { + const list = Array.isArray(node.oneOf) ? node.oneOf : Array.isArray(node.anyOf) ? node.anyOf : undefined; + if (list) + return list.filter(isRecord).filter((branch) => branch.type !== "null"); + if (Array.isArray(node.type)) { + const { type: _types, ...rest } = node; + return node.type.filter((t) => t !== "null").map((t) => ({ ...rest, type: t })); + } + return; +} +function toValue(node) { + if (!isRecord(node)) + return { type: "json" }; + const branches = variants(node); + if (branches) { + if (branches.length === 0) + return withAnnotations(node, { type: "json" }); + if (branches.length === 1) + return withAnnotations(node, toValue({ ...branches[0] })); + return withAnnotations(node, { oneOf: branches.map(toValue) }); + } + const type = node.type; + if (type === "object" || type === undefined && isRecord(node.properties)) { + const out = { + type: "object", + additionalProperties: typeof node.additionalProperties === "boolean" ? node.additionalProperties : true + }; + if (isRecord(node.properties)) + out.properties = toProperties(node.properties, node.required); + return withAnnotations(node, out); + } + if (type === "array") { + const out = { type: "array" }; + if (Object.hasOwn(node, "items")) + out.items = toValue(node.items); + return withAnnotations(node, out); + } + if (typeof type === "string" && SCALAR_TYPES.has(type)) { + const out = { type }; + if (Array.isArray(node.enum) && node.enum.length > 0) + out.enum = node.enum; + if (Object.hasOwn(node, "const")) + out.const = node.const; + return withAnnotations(node, out); + } + return withAnnotations(node, { type: "json" }); +} +function toProperties(properties, required) { + const requiredSet = new Set(Array.isArray(required) ? required.filter((k) => typeof k === "string") : []); + const out = {}; + for (const [key, value] of Object.entries(properties)) { + const converted = toValue(value); + if (requiredSet.has(key)) + converted.required = true; + out[key] = converted; + } + return out; +} +function parametersFromInputSchema(schema) { + if (!isRecord(schema) || !isRecord(schema.properties)) + return {}; + return toProperties(schema.properties, schema.required); +} + // src/netx/mcp-store.ts var STORE_KEY4 = Symbol.for("dsh-netxops.mcp-catalog-store"); var EMPTY = Object.freeze({ @@ -4879,18 +4975,6 @@ var mcpOut = { schema: { type: "object", additionalProperties: true }, render: renderMcpResult }; -function parametersFromInputSchema(schema) { - if (schema && typeof schema === "object" && !Array.isArray(schema)) { - const props = schema.properties; - if (props && typeof props === "object" && !Array.isArray(props)) { - return props; - } - if (schema.type === undefined && schema.properties === undefined) { - return schema; - } - } - return {}; -} function defineRemoteTool(tool2, timeoutMs) { const name = publicToolName(tool2.name); return defineTool2({ diff --git a/src/netx/mcp-schema.ts b/src/netx/mcp-schema.ts new file mode 100644 index 0000000..a9212fb --- /dev/null +++ b/src/netx/mcp-schema.ts @@ -0,0 +1,104 @@ +/** + * Convert netx MCP `inputSchema` (standard JSON Schema) into the DSH value + * schema DSL accepted by `defineTool({ parameters })`. + * + * The DSL is a strict subset: nodes allow only type / enum / const / + * properties / additionalProperties / items / oneOf plus annotations, and + * `required` is a per-property `true`. Range and size constraints + * (`minimum`, `maxItems`, ...) are folded into the description so the model + * still sees them; netx validates arguments server-side anyway. + */ + +type Json = Record + +const ANNOTATIONS = ['description', 'title', 'default', 'examples'] as const +const SCALAR_TYPES = new Set(['string', 'number', 'integer', 'boolean', 'null']) +const CONSTRAINTS = [ + 'minimum', + 'maximum', + 'exclusiveMinimum', + 'exclusiveMaximum', + 'multipleOf', + 'minLength', + 'maxLength', + 'pattern', + 'format', + 'minItems', + 'maxItems', + 'uniqueItems', +] as const + +function isRecord(value: unknown): value is Json { + return typeof value === 'object' && value !== null && !Array.isArray(value) +} + +function withAnnotations(source: Json, target: Json): Json { + for (const key of ANNOTATIONS) { + if (Object.hasOwn(source, key)) target[key] = source[key] + } + const notes = CONSTRAINTS.filter(key => Object.hasOwn(source, key)).map(key => `${key}=${JSON.stringify(source[key])}`) + if (notes.length > 0) { + const base = typeof target.description === 'string' ? target.description : '' + target.description = `${base}${base ? ' ' : ''}(${notes.join(', ')})` + } + return target +} + +/** Branches of anyOf / oneOf / a type array, minus `null` (nullable = optional here). */ +function variants(node: Json): Json[] | undefined { + const list = Array.isArray(node.oneOf) ? node.oneOf : Array.isArray(node.anyOf) ? node.anyOf : undefined + if (list) return list.filter(isRecord).filter(branch => branch.type !== 'null') + if (Array.isArray(node.type)) { + const { type: _types, ...rest } = node + return node.type.filter(t => t !== 'null').map(t => ({ ...rest, type: t })) + } + return undefined +} + +function toValue(node: unknown): Json { + if (!isRecord(node)) return { type: 'json' } + const branches = variants(node) + if (branches) { + if (branches.length === 0) return withAnnotations(node, { type: 'json' }) + if (branches.length === 1) return withAnnotations(node, toValue({ ...branches[0] })) + return withAnnotations(node, { oneOf: branches.map(toValue) }) + } + const type = node.type + if (type === 'object' || (type === undefined && isRecord(node.properties))) { + const out: Json = { + type: 'object', + additionalProperties: typeof node.additionalProperties === 'boolean' ? node.additionalProperties : true, + } + if (isRecord(node.properties)) out.properties = toProperties(node.properties, node.required) + return withAnnotations(node, out) + } + if (type === 'array') { + const out: Json = { type: 'array' } + if (Object.hasOwn(node, 'items')) out.items = toValue(node.items) + return withAnnotations(node, out) + } + if (typeof type === 'string' && SCALAR_TYPES.has(type)) { + const out: Json = { type } + if (Array.isArray(node.enum) && node.enum.length > 0) out.enum = node.enum + if (Object.hasOwn(node, 'const')) out.const = node.const + return withAnnotations(node, out) + } + return withAnnotations(node, { type: 'json' }) +} + +function toProperties(properties: Json, required: unknown): Json { + const requiredSet = new Set(Array.isArray(required) ? required.filter(k => typeof k === 'string') : []) + const out: Json = {} + for (const [key, value] of Object.entries(properties)) { + const converted = toValue(value) + if (requiredSet.has(key)) converted.required = true + out[key] = converted + } + return out +} + +/** `defineTool` parameters (a property map) from an MCP tool `inputSchema`. */ +export function parametersFromInputSchema(schema: unknown): Json { + if (!isRecord(schema) || !isRecord(schema.properties)) return {} + return toProperties(schema.properties, schema.required) +} diff --git a/src/netx/tools.ts b/src/netx/tools.ts index 037eaa2..7deda18 100644 --- a/src/netx/tools.ts +++ b/src/netx/tools.ts @@ -14,6 +14,7 @@ import { defineTool } from '@deepseek-ai/dsh-tools' import type { NetxCapabilityGroupId, NetxCapabilityGroups } from './capability-groups.ts' import { resolveEnabledTools } from './mcp-catalog.ts' import { callMcpTool, mcpContentText, mcpEndpointUrl } from './mcp-remote.ts' +import { parametersFromInputSchema } from './mcp-schema.ts' import { getMcpCatalogTools } from './mcp-store.ts' import { publicToolName, type NetxMcpToolDescriptor } from './mcp-types.ts' import { getNetxConnection } from './runtime.ts' @@ -73,24 +74,6 @@ const mcpOut = { render: renderMcpResult, } -/** - * `defineTool` expects a property map (DSH ParameterSchemaSpec), not a full - * JSON Schema document. MCP `inputSchema` is usually `{ type:'object', properties }`. - */ -function parametersFromInputSchema(schema: Record): Record { - if (schema && typeof schema === 'object' && !Array.isArray(schema)) { - const props = schema.properties - if (props && typeof props === 'object' && !Array.isArray(props)) { - return props as Record - } - // Already a property map (legacy / odd servers). - if (schema.type === undefined && schema.properties === undefined) { - return schema - } - } - return {} -} - function defineRemoteTool( tool: NetxMcpToolDescriptor, timeoutMs: number, diff --git a/test/fixtures-netx-mcp-tools.json b/test/fixtures-netx-mcp-tools.json new file mode 100644 index 0000000..8594f6c --- /dev/null +++ b/test/fixtures-netx-mcp-tools.json @@ -0,0 +1,1990 @@ +[ + { + "name": "queryNmsAlarms", + "inputSchema": { + "properties": { + "severity": { + "type": "string", + "description": "critical/major/minor/warning" + }, + "is_cleared": { + "type": "string", + "description": "\"False\" = active only; \"True\" = cleared." + }, + "ne_id": { + "type": "string", + "description": "Filter only; do not show UUID to users" + }, + "host_name": { + "type": "string", + "description": "Filter by NE host_name" + }, + "keyword": { + "type": "string", + "description": "Substring on cause/object/event. Examples: LOS, Fiber Break, bandwidth, CRC, BN EMS, dying gasp, License, optical power, BGP Neighbour." + }, + "time_from": { + "type": "string", + "description": "ISO time; filters last_seen_at >=" + }, + "time_to": { + "type": "string", + "description": "ISO time; filters last_seen_at <=" + }, + "page": { + "type": "integer", + "minimum": 1, + "default": 1 + }, + "page_size": { + "type": "integer", + "minimum": 1, + "maximum": 500, + "default": 50 + } + }, + "required": [], + "type": "object", + "additionalProperties": false + } + }, + { + "name": "aggregateNmsAlarms", + "inputSchema": { + "properties": { + "severity": { + "type": "string", + "description": "Optional perceived_severity filter (critical/major/minor/warning)." + }, + "top_ne": { + "type": "integer", + "minimum": 0, + "maximum": 5000, + "default": 50, + "description": "Max NE buckets to return (0 = all; positive limit up to 5000). Ignored when group_by is set." + }, + "exclude_missing_host": { + "type": "boolean", + "default": true, + "description": "Omit (host_name missing) from by_ne / host rankings." + }, + "time_from": { + "type": "string", + "description": "ISO time; filters last_seen_at >=" + }, + "time_to": { + "type": "string", + "description": "ISO time; filters last_seen_at <=" + }, + "group_by": { + "type": "string", + "enum": [ + "alarm_alarm_key", + "alarm_host_name", + "alarm_ne_id", + "alarm_object_name", + "alarm_event_type", + "alarm_native_probable_cause", + "alarm_perceived_severity", + "alarm_is_cleared", + "alarm_time_created", + "alarm_root_cause_alarm_indication", + "ne_ne_id", + "ne_ne_name", + "ne_user_label", + "ne_ip_address", + "ne_ipv6_address", + "ne_ne_type", + "ne_device_level", + "ne_host_name", + "ne_location", + "ne_hardware_version", + "ne_loopback", + "ne_consistent_state", + "ne_interface_version", + "ne_mac", + "ne_admin_status", + "ne_address_type", + "ne_connection_status", + "ne_maintain_status", + "ne_net_mask", + "ne_create_time", + "ne_creator", + "ne_vendor", + "ne_source_type", + "ne_exists" + ], + "description": "Optional. When set, routes to dynamic raw aggregation (same as aggregateNmsAlarmsRaw). Prefer alarm_host_name for NE Top." + }, + "group_by2": { + "type": "string", + "enum": [ + "alarm_alarm_key", + "alarm_host_name", + "alarm_ne_id", + "alarm_object_name", + "alarm_event_type", + "alarm_native_probable_cause", + "alarm_perceived_severity", + "alarm_is_cleared", + "alarm_time_created", + "alarm_root_cause_alarm_indication", + "ne_ne_id", + "ne_ne_name", + "ne_user_label", + "ne_ip_address", + "ne_ipv6_address", + "ne_ne_type", + "ne_device_level", + "ne_host_name", + "ne_location", + "ne_hardware_version", + "ne_loopback", + "ne_consistent_state", + "ne_interface_version", + "ne_mac", + "ne_admin_status", + "ne_address_type", + "ne_connection_status", + "ne_maintain_status", + "ne_net_mask", + "ne_create_time", + "ne_creator", + "ne_vendor", + "ne_source_type", + "ne_exists" + ], + "description": "Optional second group field when group_by is set." + }, + "is_cleared": { + "type": "string", + "description": "Only used when group_by is set." + }, + "ne_id": { + "type": "string", + "description": "Only used when group_by is set." + }, + "event_type": { + "type": "string", + "description": "Only used when group_by is set." + }, + "keyword": { + "type": "string", + "description": "Only used when group_by is set." + }, + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 2000, + "default": 200, + "description": "Bucket limit when group_by is set (default 200)." + } + }, + "required": [], + "type": "object", + "additionalProperties": false + } + }, + { + "name": "runNmsDiagnostics", + "inputSchema": { + "properties": {}, + "required": [], + "type": "object", + "additionalProperties": false + } + }, + { + "name": "queryNmsNeInventory", + "inputSchema": { + "properties": { + "keyword": { + "type": "string" + }, + "page": { + "type": "integer", + "minimum": 1, + "default": 1 + }, + "page_size": { + "type": "integer", + "minimum": 1, + "maximum": 500, + "default": 50 + } + }, + "required": [], + "type": "object", + "additionalProperties": false + } + }, + { + "name": "getNmsNe", + "inputSchema": { + "properties": { + "ne_id": { + "type": "string", + "description": "NMS UUID, or exact host_name / IP." + } + }, + "required": [ + "ne_id" + ], + "type": "object", + "additionalProperties": false + } + }, + { + "name": "queryNmsAlarmsRaw", + "inputSchema": { + "properties": { + "severity": { + "type": "string" + }, + "is_cleared": { + "type": "string" + }, + "ne_id": { + "type": "string" + }, + "event_type": { + "type": "string" + }, + "keyword": { + "type": "string", + "description": "Substring filter; see queryNmsAlarms keyword examples." + }, + "time_from": { + "type": "string" + }, + "time_to": { + "type": "string" + }, + "order_by": { + "type": "string", + "enum": [ + "last_seen_at", + "time_created", + "perceived_severity", + "event_type", + "ne_id" + ] + }, + "order": { + "type": "string", + "enum": [ + "asc", + "desc" + ] + }, + "select_fields": { + "type": "array", + "items": { + "type": "string" + } + }, + "field_preset": { + "type": "string", + "enum": [ + "brief", + "evidence", + "ne_debug" + ] + }, + "page": { + "type": "integer", + "minimum": 1, + "default": 1 + }, + "page_size": { + "type": "integer", + "minimum": 1, + "maximum": 500, + "default": 50 + } + }, + "required": [], + "type": "object", + "additionalProperties": false + } + }, + { + "name": "aggregateNmsAlarmsRaw", + "inputSchema": { + "properties": { + "group_by": { + "type": "string", + "enum": [ + "alarm_alarm_key", + "alarm_host_name", + "alarm_ne_id", + "alarm_object_name", + "alarm_event_type", + "alarm_native_probable_cause", + "alarm_perceived_severity", + "alarm_is_cleared", + "alarm_time_created", + "alarm_root_cause_alarm_indication", + "ne_ne_id", + "ne_ne_name", + "ne_user_label", + "ne_ip_address", + "ne_ipv6_address", + "ne_ne_type", + "ne_device_level", + "ne_host_name", + "ne_location", + "ne_hardware_version", + "ne_loopback", + "ne_consistent_state", + "ne_interface_version", + "ne_mac", + "ne_admin_status", + "ne_address_type", + "ne_connection_status", + "ne_maintain_status", + "ne_net_mask", + "ne_create_time", + "ne_creator", + "ne_vendor", + "ne_source_type", + "ne_exists" + ] + }, + "group_by2": { + "type": "string", + "enum": [ + "alarm_alarm_key", + "alarm_host_name", + "alarm_ne_id", + "alarm_object_name", + "alarm_event_type", + "alarm_native_probable_cause", + "alarm_perceived_severity", + "alarm_is_cleared", + "alarm_time_created", + "alarm_root_cause_alarm_indication", + "ne_ne_id", + "ne_ne_name", + "ne_user_label", + "ne_ip_address", + "ne_ipv6_address", + "ne_ne_type", + "ne_device_level", + "ne_host_name", + "ne_location", + "ne_hardware_version", + "ne_loopback", + "ne_consistent_state", + "ne_interface_version", + "ne_mac", + "ne_admin_status", + "ne_address_type", + "ne_connection_status", + "ne_maintain_status", + "ne_net_mask", + "ne_create_time", + "ne_creator", + "ne_vendor", + "ne_source_type", + "ne_exists" + ] + }, + "severity": { + "type": "string" + }, + "is_cleared": { + "type": "string" + }, + "ne_id": { + "type": "string" + }, + "event_type": { + "type": "string" + }, + "keyword": { + "type": "string" + }, + "time_from": { + "type": "string" + }, + "time_to": { + "type": "string" + }, + "exclude_missing_host": { + "type": "boolean", + "default": true, + "description": "Omit missing host buckets when grouping by host/user_label fields." + }, + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 2000, + "default": 200 + } + }, + "required": [ + "group_by" + ], + "type": "object", + "additionalProperties": false + } + }, + { + "name": "listNmsAlarmFields", + "inputSchema": { + "properties": {}, + "required": [], + "type": "object", + "additionalProperties": false + } + }, + { + "name": "sqlQueryNms", + "inputSchema": { + "properties": { + "sql": { + "type": "string" + }, + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 2000, + "default": 200 + }, + "statement_timeout_ms": { + "type": "integer", + "minimum": 0, + "maximum": 30000, + "default": 0 + } + }, + "required": [ + "sql" + ], + "type": "object", + "additionalProperties": false + } + }, + { + "name": "listManagedNe", + "inputSchema": { + "properties": { + "keyword": { + "type": "string" + }, + "vendor": { + "type": "string" + }, + "connect_status": { + "type": "string", + "enum": [ + "unknown", + "testing", + "pass", + "fail" + ] + }, + "page": { + "type": "integer", + "minimum": 1, + "default": 1 + }, + "page_size": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 20 + } + }, + "required": [], + "type": "object", + "additionalProperties": false + } + }, + { + "name": "getManagedNe", + "inputSchema": { + "properties": { + "ne_id": { + "type": "string", + "description": "Managed NE id (not NMS host_name / not NMS ne_id unless they coincide)." + } + }, + "required": [ + "ne_id" + ], + "type": "object", + "additionalProperties": false + } + }, + { + "name": "execManagedNe", + "inputSchema": { + "properties": { + "ne_id": { + "type": "string" + }, + "nms_ne_id": { + "type": "string", + "description": "NMS inventory id (preferred)." + }, + "ne_ids": { + "type": "array", + "items": { + "type": "string" + }, + "maxItems": 20, + "description": "Managed NE ids for concurrent batch (shared commands)." + }, + "nms_ne_ids": { + "type": "array", + "items": { + "type": "string" + }, + "maxItems": 20, + "description": "NMS inventory ne_ids for concurrent batch (shared commands)." + }, + "targets": { + "type": "array", + "maxItems": 20, + "items": { + "type": "object", + "properties": { + "ne_id": { + "type": "string" + }, + "nms_ne_id": { + "type": "string" + }, + "commands": { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 1, + "maxItems": 5 + } + }, + "additionalProperties": false + }, + "description": "Preferred for mixed-vendor / per-NE command sets: each item is one NE (ne_id OR nms_ne_id) with its own commands[]. Per-target commands override top-level shared commands. Still one concurrent batch — not N single-NE calls." + }, + "commands": { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 1, + "maxItems": 5, + "description": "Commands for single NE, or shared commands for ne_ids/nms_ne_ids. Optional fallback for targets that omit per-target commands." + }, + "read_timeout_sec": { + "type": "integer", + "minimum": 10, + "maximum": 120, + "default": 60, + "description": "Per-command read timeout (default 60; use 90–120 for slow show)." + }, + "concurrency": { + "type": "integer", + "minimum": 1, + "maximum": 8, + "default": 4, + "description": "Parallel NEs for batch mode (ignored for single-NE)." + }, + "async": { + "type": "boolean", + "description": "true=background job_id + getNeExecJob; false=force sync; omit=auto for large batches (~4+ NEs, NETX_NE_EXEC_ASYNC_MIN_NES)." + } + }, + "required": [], + "type": "object", + "additionalProperties": false + } + }, + { + "name": "getNeExecJob", + "inputSchema": { + "properties": { + "job_id": { + "type": "string", + "description": "Job id from execManagedNe async response." + } + }, + "required": [ + "job_id" + ], + "type": "object", + "additionalProperties": false + } + }, + { + "name": "listCliTargets", + "inputSchema": { + "properties": { + "source": { + "type": "string", + "enum": [ + "managed", + "nms", + "all" + ], + "default": "all", + "description": "nms selects the configured NMS inventory; managed selects managed NEs." + }, + "keyword": { + "type": "string" + }, + "page": { + "type": "integer", + "minimum": 1, + "default": 1 + }, + "page_size": { + "type": "integer", + "minimum": 1, + "maximum": 500, + "default": 50 + } + }, + "required": [], + "type": "object", + "additionalProperties": false + } + }, + { + "name": "findTopologyPaths", + "inputSchema": { + "properties": { + "from_nms_ne_id": { + "type": "string", + "description": "Source NMS ne_id (from alarm ne_id); mutually exclusive with from_managed_ne_id" + }, + "from_managed_ne_id": { + "type": "string", + "description": "Source managed NE id; mutually exclusive with from_nms_ne_id" + }, + "to_nms_ne_id": { + "type": "string", + "description": "Target NMS ne_id; mutually exclusive with to_managed_ne_id" + }, + "to_managed_ne_id": { + "type": "string", + "description": "Target managed NE id; mutually exclusive with to_nms_ne_id" + }, + "max_paths": { + "type": "integer", + "minimum": 1, + "maximum": 10, + "default": 3 + }, + "max_hops": { + "type": "integer", + "minimum": 1, + "maximum": 12, + "default": 6 + }, + "layer": { + "type": "string", + "default": "physical" + }, + "detail": { + "type": "string", + "enum": [ + "summary", + "full" + ], + "default": "summary", + "description": "summary=compact ops fields; full=include attrs/coords." + } + }, + "required": [], + "type": "object", + "additionalProperties": false + } + }, + { + "name": "getTopologyTree", + "inputSchema": { + "properties": { + "compact": { + "type": "boolean", + "default": true, + "description": "Default true — slim folder/view fields. Pass false for raw API tree." + }, + "max_depth": { + "type": "integer", + "minimum": 0, + "description": "Optional depth limit from root (0 = root only). Omitted = full depth." + } + }, + "required": [], + "type": "object", + "additionalProperties": false + } + }, + { + "name": "getTopologyView", + "inputSchema": { + "properties": { + "view_id": { + "type": "string" + }, + "detail": { + "type": "string", + "enum": [ + "summary", + "full" + ], + "default": "summary" + }, + "sample": { + "type": "integer", + "minimum": 0, + "maximum": 200, + "default": 20, + "description": "Sample node count when detail=summary (use >=node_count for layout study)." + } + }, + "required": [ + "view_id" + ], + "type": "object", + "additionalProperties": false + } + }, + { + "name": "createTopologyFolder", + "inputSchema": { + "properties": { + "name": { + "type": "string", + "description": "Root or sub-region display name" + }, + "parent_id": { + "type": "string", + "description": "Omit for new top-level「根」+根图. Pass 根图/region folder id to create a sub-region under it (not a sibling custom view)." + }, + "locale": { + "type": "string", + "description": "Optional zh|en — labels auto「根图」vs Root map" + }, + "sort_order": { + "type": "integer", + "default": 0 + } + }, + "required": [ + "name" + ], + "type": "object", + "additionalProperties": false + } + }, + { + "name": "addTopologyViewNodes", + "inputSchema": { + "properties": { + "view_id": { + "type": "string" + }, + "max_nodes": { + "type": "integer", + "minimum": 1, + "maximum": 2000, + "description": "Optional: PATCH this view's membership.max_nodes before add (use on physical 根图/region when soft cap blocks members)." + }, + "keyword": { + "type": "string" + }, + "role": { + "type": "string" + }, + "vendor": { + "type": "string" + }, + "link_status": { + "type": "string", + "enum": [ + "linked", + "orphaned", + "managed", + "ume", + "both" + ] + }, + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 2000, + "default": 500 + }, + "offset": { + "type": "integer", + "minimum": 0, + "default": 0 + }, + "fabric_node_ids": { + "type": "array", + "items": { + "type": "string" + } + }, + "layout": { + "type": "string", + "enum": [ + "grid", + "keep" + ], + "default": "grid" + } + }, + "required": [ + "view_id" + ], + "type": "object", + "additionalProperties": false + } + }, + { + "name": "removeTopologyViewNodes", + "inputSchema": { + "properties": { + "view_id": { + "type": "string" + }, + "keyword": { + "type": "string" + }, + "role": { + "type": "string" + }, + "vendor": { + "type": "string" + }, + "link_status": { + "type": "string", + "enum": [ + "linked", + "orphaned", + "managed", + "ume", + "both" + ] + }, + "fabric_node_ids": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "view_id" + ], + "type": "object", + "additionalProperties": false + } + }, + { + "name": "sinkTopologyDualUnits", + "inputSchema": { + "properties": { + "source_view_id": { + "type": "string", + "description": "Root / source physical view_id to drain." + }, + "sink_view_id": { + "type": "string", + "description": "Child-region physical view_id receiving batches." + }, + "max_units": { + "type": "integer", + "minimum": 1, + "maximum": 20, + "default": 3, + "description": "Max dual_units per batch (default 3)." + }, + "min_nodes": { + "type": "integer", + "minimum": 2, + "maximum": 200, + "default": 8 + }, + "max_nodes": { + "type": "integer", + "minimum": 2, + "maximum": 400, + "default": 300, + "description": "Max nodes per dual_unit candidate (CN eyes often 150–300)." + }, + "max_batch_nodes": { + "type": "integer", + "minimum": 8, + "maximum": 800, + "default": 400 + }, + "layout_batch": { + "type": "boolean", + "default": true, + "description": "Default true: run layout_dual_unit per unit before parking (old staging/block layout). false = copy source coords only." + }, + "unit_gap": { + "type": "number", + "default": 220, + "description": "Gap between laid-out units in a batch strip." + }, + "until_empty": { + "type": "boolean", + "default": false, + "description": "Loop batches until source empty (watch MCP timeout)." + }, + "max_batches": { + "type": "integer", + "minimum": 1, + "maximum": 200, + "description": "Safety cap when until_empty (default 50)." + }, + "include_leftovers": { + "type": "boolean", + "default": true, + "description": "When dual_units exhausted, move leftover NE chunks." + }, + "keep_layers": { + "type": "array", + "items": { + "type": "string" + }, + "default": [ + "core", + "agg" + ], + "description": "Fabric layers that stay on source (not sunk). Default core+agg so root keeps the gravity bar; pass [] to drain everything (legacy)." + }, + "sink_layers": { + "type": "array", + "items": { + "type": "string" + }, + "default": [ + "access", + "other" + ], + "description": "Only move these layers this phase. Default access+other. Pass [] to allow any non-keep. With by_level=true, auto picks deepest remaining (access→agg→…)." + }, + "by_level": { + "type": "boolean", + "default": false, + "description": "Auto sink deepest remaining non-keep layer each batch (access first, then agg if keep allows)." + }, + "prefer_pure": { + "type": "boolean", + "default": false, + "description": "Legacy: prefer both portals sinkable. Default false so CN/core eyes (portals stay on root) are not deprioritized." + }, + "prefer_core_eye": { + "type": "boolean", + "default": true, + "description": "Alias of prefer_top_eye. Prefer core/agg eyes top→down before access rings." + }, + "prefer_top_eye": { + "type": "boolean", + "default": true, + "description": "Prefer dual_unit portals top→down: core–core, core–agg, agg–agg, then maximize movable coverage." + }, + "dry_run": { + "type": "boolean", + "default": false + }, + "pad": { + "type": "number", + "default": 280, + "description": "Gap between sink hull and parked batch." + }, + "detect_max_units": { + "type": "integer", + "minimum": 20, + "maximum": 300, + "default": 120 + } + }, + "required": [ + "source_view_id", + "sink_view_id" + ], + "type": "object", + "additionalProperties": false + } + }, + { + "name": "copyTopologyViewNodes", + "inputSchema": { + "properties": { + "source_view_id": { + "type": "string", + "description": "Canvas to copy FROM." + }, + "target_view_id": { + "type": "string", + "description": "Canvas to copy TO." + }, + "copy_positions": { + "type": "boolean", + "default": true, + "description": "Copy x/y from source (default true)." + }, + "clear_target": { + "type": "boolean", + "default": false, + "description": "Remove existing target members before copy." + }, + "offset_x": { + "type": "number", + "default": 0, + "description": "Shift copied x by this amount." + }, + "offset_y": { + "type": "number", + "default": 0 + }, + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 2000, + "description": "Optional cap on how many source nodes to copy." + }, + "dry_run": { + "type": "boolean", + "default": false + } + }, + "required": [ + "source_view_id", + "target_view_id" + ], + "type": "object", + "additionalProperties": false + } + }, + { + "name": "updateTopologyViewPositions", + "inputSchema": { + "properties": { + "view_id": { + "type": "string" + }, + "layout": { + "type": "string", + "enum": [ + "grid", + "offset", + "stack" + ] + }, + "keyword": { + "type": "string" + }, + "role": { + "type": "string" + }, + "vendor": { + "type": "string" + }, + "link_status": { + "type": "string", + "enum": [ + "linked", + "orphaned", + "managed", + "ume", + "both" + ] + }, + "fabric_node_ids": { + "type": "array", + "items": { + "type": "string" + } + }, + "origin_x": { + "type": "number", + "default": 40 + }, + "origin_y": { + "type": "number", + "default": 40 + }, + "gap_x": { + "type": "number", + "default": 180 + }, + "gap_y": { + "type": "number", + "default": 120 + }, + "cols": { + "type": "integer", + "minimum": 0, + "default": 0 + }, + "dx": { + "type": "number", + "default": 0 + }, + "dy": { + "type": "number", + "default": 0 + }, + "positions": { + "type": "array", + "items": { + "type": "object", + "properties": { + "fabric_node_id": { + "type": "string" + }, + "x": { + "type": "number" + }, + "y": { + "type": "number" + }, + "label": { + "type": "string", + "description": "Omit to keep the current label; \"\" clears a rename (follow device name)." + }, + "locked": { + "type": "boolean", + "description": "Omit to keep the lock. Locked nodes are skipped unless locked=false (unlock + move)." + } + }, + "required": [ + "fabric_node_id" + ], + "additionalProperties": false + } + } + }, + "required": [ + "view_id" + ], + "type": "object", + "additionalProperties": false + } + }, + { + "name": "projectTopologyNeighbors", + "inputSchema": { + "properties": { + "view_id": { + "type": "string" + }, + "seed_fabric_node_ids": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Optional fabric node ids already on the view to expand from" + }, + "managed_ne_ids": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Optional managed NE ids to resolve to on-view fabric seeds" + }, + "region_folder_id": { + "type": "string", + "description": "If set, only add neighbors with matching fabric.region_folder_id; skipped peers are counted in out_of_region_skipped." + }, + "dry_run": { + "type": "boolean", + "default": false, + "description": "Preview projected graph without saving placements." + }, + "detail": { + "type": "string", + "enum": [ + "summary", + "full" + ], + "default": "summary" + }, + "sample": { + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 20 + } + }, + "required": [ + "view_id" + ], + "type": "object", + "additionalProperties": false + } + }, + { + "name": "queryTopologyFabricNodes", + "inputSchema": { + "properties": { + "mode": { + "type": "string", + "enum": [ + "summary", + "list", + "search" + ], + "description": "Omit to auto-pick: q→search, else list." + }, + "q": { + "type": "string", + "description": "Search needle (mode=search); also aliases keyword for list." + }, + "keyword": { + "type": "string", + "description": "List filter keyword; alias of q for search." + }, + "role": { + "type": "string" + }, + "level": { + "type": "string", + "description": "Exact fabric level e.g. 1.1" + }, + "level_major": { + "type": "string", + "description": "Major band e.g. 2 → [2.0, 3.0)" + }, + "region_folder_id": { + "type": "string", + "description": "Filter by topo folder id (UME region / canvas folder)." + }, + "link_status": { + "type": "string", + "enum": [ + "linked", + "orphaned", + "managed", + "ume", + "both" + ] + }, + "page": { + "type": "integer", + "minimum": 1, + "default": 1 + }, + "page_size": { + "type": "integer", + "minimum": 1, + "maximum": 500, + "default": 50 + }, + "limit": { + "type": "integer", + "description": "Alias of page_size" + }, + "summary": { + "type": "boolean", + "default": false, + "description": "Shortcut for mode=summary" + } + }, + "required": [], + "type": "object", + "additionalProperties": false + } + }, + { + "name": "classifyTopologyFabricNodes", + "inputSchema": { + "properties": { + "action": { + "type": "string", + "enum": [ + "match", + "tag", + "patch", + "unmatched", + "preview_rules", + "apply_rules", + "list_rules" + ], + "description": "Required. Typical: match → tag(dry_run) → tag." + }, + "pattern": { + "type": "string", + "description": "Regex for match/tag (aliases q)." + }, + "q": { + "type": "string", + "description": "Alias of pattern." + }, + "match_field": { + "type": "string", + "enum": [ + "name", + "ip", + "name_ip" + ], + "default": "name" + }, + "sample_limit": { + "type": "integer", + "minimum": 1, + "maximum": 200, + "default": 50 + }, + "fabric_node_ids": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Explicit ids for tag (from match)." + }, + "fabric_node_id": { + "type": "string", + "description": "Single id for patch (alias node_id)." + }, + "node_id": { + "type": "string" + }, + "level": { + "description": "Layout rank e.g. 0, 1, 1.1, 2.1, 3. null clears." + }, + "role": { + "type": "string", + "description": "Preset alias → level: external|core|aggregation|access|edge." + }, + "region_folder_id": { + "type": "string", + "description": "Topo region folder id; empty + clear_region clears." + }, + "clear_region": { + "type": "boolean", + "default": false, + "description": "Clear region_folder_id on tag/patch." + }, + "dry_run": { + "type": "boolean", + "default": false, + "description": "tag only: preview without write." + }, + "kind": { + "type": "string", + "enum": [ + "any", + "level", + "role", + "region" + ], + "default": "any", + "description": "unmatched filter (role aliases level)." + }, + "page": { + "type": "integer", + "minimum": 1, + "default": 1 + }, + "page_size": { + "type": "integer", + "minimum": 1, + "maximum": 500, + "default": 50 + }, + "limit": { + "type": "integer", + "description": "Alias of page_size" + }, + "skip_manual": { + "type": "boolean", + "default": true, + "description": "apply_rules: skip manually tagged nodes." + }, + "overwrite_manual": { + "type": "boolean", + "default": false, + "description": "apply_rules shortcut: skip_manual=false." + }, + "fill_empty_only": { + "type": "boolean", + "default": false, + "description": "apply_rules: only fill empty role/region." + } + }, + "required": [ + "action" + ], + "type": "object", + "additionalProperties": false + } + }, + { + "name": "queryTopologyNeighborhood", + "inputSchema": { + "properties": { + "node_id": { + "type": "string" + }, + "depth": { + "type": "integer", + "minimum": 1, + "maximum": 3, + "default": 1 + }, + "layer": { + "type": "string", + "default": "physical" + } + }, + "required": [ + "node_id" + ], + "type": "object", + "additionalProperties": false + } + }, + { + "name": "queryTopologyEdges", + "inputSchema": { + "properties": { + "node_id": { + "type": "string" + }, + "keyword": { + "type": "string" + }, + "layer": { + "type": "string", + "default": "physical" + }, + "status": { + "type": "string", + "enum": [ + "active", + "missing", + "stale" + ] + }, + "source": { + "type": "string", + "enum": [ + "lldp", + "ume", + "manual" + ] + }, + "detail": { + "type": "string", + "enum": [ + "adjacency", + "ports" + ], + "default": "adjacency", + "description": "adjacency (default) = NE pairs; ports = raw port rows in items[]." + }, + "page": { + "type": "integer", + "minimum": 1, + "default": 1 + }, + "page_size": { + "type": "integer", + "minimum": 1, + "maximum": 500, + "default": 100 + } + }, + "required": [], + "type": "object", + "additionalProperties": false + } + }, + { + "name": "suggestSinkHubs", + "inputSchema": { + "properties": { + "source_view_id": { + "type": "string", + "description": "Root / leftover canvas (required). Alias: view_id." + }, + "view_id": { + "type": "string", + "description": "Alias of source_view_id." + }, + "sink_view_id": { + "type": "string", + "description": "Eye sink canvas; ids already there are dropped from batches." + }, + "exclude_portal_ids": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Extra freeze ids (CN portals). Merged with dual_units portals." + }, + "use_source_dual_portals": { + "type": "boolean", + "default": false, + "description": "Exclude portals from source dual_units (default false)." + }, + "use_sink_dual_portals": { + "type": "boolean", + "default": true, + "description": "Exclude portals from sink dual_units when sink_view_id set." + }, + "only_layers": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Hub layers to consider (default [agg, core])." + }, + "min_territory": { + "type": "integer", + "minimum": 0, + "default": 1, + "description": "Skip hubs with remaining_territory below this." + }, + "min_move_n": { + "type": "integer", + "minimum": 1, + "default": 1 + }, + "top_n": { + "type": "integer", + "minimum": 1, + "maximum": 40, + "default": 12 + }, + "pick": { + "type": "integer", + "minimum": 1, + "default": 1, + "description": "1-based batch to expose as batch + move_nodes." + }, + "include_hub": { + "type": "boolean", + "default": true, + "description": "Include hub id in fabric_node_ids when still on source." + } + }, + "required": [], + "type": "object", + "additionalProperties": false + } + }, + { + "name": "analyzeTopologyViewLayout", + "inputSchema": { + "properties": { + "view_id": { + "type": "string", + "description": "Analyze a single topology view" + }, + "folder_id": { + "type": "string", + "description": "Sample physical views under this folder (tree walk)" + }, + "detail": { + "type": "string", + "enum": [ + "summary", + "structure", + "hotspots", + "blocks", + "both" + ], + "default": "summary", + "description": "summary=score only; structure=gravity/hubs/recipe hint (phase 0.5); hotspots|blocks=sight; both=structure+sight (hand-drag)" + }, + "score_profile": { + "type": "string", + "enum": [ + "auto", + "default", + "eye" + ], + "default": "auto", + "description": "Scoring weights. auto→eye for large sparse diagonal canvases; eye softens axis/rings (arc-band dual sinks); default=metro." + }, + "sight_limit": { + "type": "integer", + "minimum": 5, + "maximum": 80, + "default": 40, + "description": "Max crossings / drag candidates when detail≠summary" + }, + "sight_cell": { + "type": "number", + "default": 600, + "description": "Grid cell size (px) for detail=blocks" + }, + "max_views": { + "type": "integer", + "minimum": 1, + "maximum": 80, + "default": 25 + }, + "min_nodes": { + "type": "integer", + "minimum": 0, + "default": 5 + }, + "max_nodes": { + "type": "integer", + "minimum": 1, + "maximum": 2000, + "default": 800 + }, + "with_meta": { + "type": "boolean", + "default": false, + "description": "Include IP/vendor caption line in overlap boxes" + } + }, + "required": [], + "type": "object", + "additionalProperties": false + } + }, + { + "name": "layoutTopologyView", + "inputSchema": { + "properties": { + "view_id": { + "type": "string", + "description": "Target canvas to layout (and write when mode=apply)" + }, + "action": { + "type": "string", + "enum": [ + "layout", + "fix_overlaps", + "layout_dual_unit", + "straighten_channels", + "untangle", + "polish_crossings", + "clear_edge_hits", + "compact_bbox", + "pull_far_chains", + "align_reference", + "orbit_sweep", + "level_bands", + "move_nodes", + "job_status", + "job_cancel" + ], + "default": "layout", + "description": "layout=full recipe; layout_dual_unit=dual-portal eye; level_bands=horizontal layer bands; move_nodes=migrate fabric_node_ids; orbit_sweep=polar sweep; polish_crossings=one-shot cut crossings; clear_edge_hits=eject edge hits (no crossing rise); pull_far_chains=scale far corridors toward portal mid; compact_bbox=gated farthest-K shrink; align_reference=same-network A/B debug only; fix_overlaps; untangle/straighten_channels; job_status|job_cancel." + }, + "source_view_id": { + "type": "string", + "description": "For action=layout: optional load graph (default=view_id). For action=move_nodes: required FROM canvas (view_id is TO). For action=align_reference: same-network reference (debug only)." + }, + "recipe": { + "type": "string", + "enum": [ + "rings", + "corridor", + "compact", + "unstick" + ], + "default": "rings", + "description": "Only for action=layout. rings=petals/min-rings; corridor/compact/unstick=Tutte corridor variants." + }, + "preset": { + "type": "string", + "enum": [ + "loose", + "balanced", + "dense" + ], + "default": "balanced" + }, + "mode": { + "type": "string", + "enum": [ + "preview", + "apply" + ], + "default": "preview", + "description": "preview=no write; apply=PATCH positions" + }, + "tune": { + "type": "boolean", + "default": false, + "description": "Small param sweep for action=layout; zero-overlap first" + }, + "params": { + "type": "object", + "description": "Overrides. layout: target_nn/target_util/…. job_status|job_cancel: job_id (required). layout_dual_unit: unit_id / portal_a+portal_b; require_zero_cross (default false). untangle: max_rounds/max_degree/protect_rigid/focus_ids[]. polish_crossings: portal_ids[]/source_view_ids[], top_n/max_moves/max_sweeps/straighten/untangle_rounds. clear_edge_hits: top_n/thr/margin/max_moves/portal_ids/max_eject_degree/preserve_axis. compact_bbox|pull_far_chains: portal_ids; pull scales toward portal mid (not outer hub); compact defaults farthest-K. orbit_sweep: node_id/pick/round/until_limit/portal_ids[]/freeze_layers[]/freeze_levels[]/max_degree/max_moves/stall_limit/max_stretch/min_delta/max_jump/angle_step/nn_floor; until_limit defaults protect_rigid=portals + objective=crossing; single-node/round default protect_rigid=off. move_nodes: fabric_node_ids[] (required), copy_positions (default true), park, remove_from_source (default true), pad/offset_x/offset_y.", + "properties": { + "job_id": { + "type": "string", + "description": "Required for job_status/job_cancel; returned by background start." + }, + "background": { + "type": "boolean", + "description": "Run apply computation in a durable subprocess and return job_id immediately." + }, + "sync": { + "type": "boolean", + "description": "Force inline computation, disabling automatic background execution." + } + }, + "additionalProperties": true + }, + "catalog": { + "type": "boolean", + "default": false, + "description": "If true, only return action/recipe/preset catalog" + } + }, + "required": [], + "type": "object", + "additionalProperties": false + } + }, + { + "name": "listBizMonitors", + "inputSchema": { + "properties": { + "kind": { + "type": "string", + "enum": [ + "all", + "projects", + "tasks" + ], + "default": "all" + }, + "purpose": { + "type": "string", + "description": "任务用途:portrait / cutover_hf;空为全部" + }, + "status": { + "type": "string", + "description": "本地过滤状态,如 active/paused" + }, + "q": { + "type": "string", + "description": "名称、设备、备注或 ID 关键词" + }, + "limit": { + "type": "integer", + "default": 100, + "minimum": 1, + "maximum": 500 + } + }, + "required": [], + "type": "object", + "additionalProperties": false + } + }, + { + "name": "listBizMonitorBatches", + "inputSchema": { + "properties": { + "kind": { + "type": "string", + "enum": [ + "auto", + "all", + "cutover", + "collect", + "runs" + ], + "default": "auto" + }, + "project_id": { + "type": "string", + "description": "" + }, + "task_id": { + "type": "string", + "description": "" + }, + "batch_id": { + "type": "string", + "description": "割接批次 ID,非采集批次 ID" + }, + "include_runs": { + "type": "boolean", + "default": false + }, + "status": { + "type": "string", + "description": "" + }, + "limit": { + "type": "integer", + "default": 50, + "minimum": 1, + "maximum": 200 + }, + "runs_limit": { + "type": "integer", + "default": 10, + "minimum": 1, + "maximum": 50 + } + }, + "required": [], + "type": "object", + "additionalProperties": false + } + }, + { + "name": "getBizMonitorContext", + "inputSchema": { + "properties": { + "project_id": { + "type": "string", + "description": "" + }, + "task_id": { + "type": "string", + "description": "" + } + }, + "required": [], + "type": "object", + "additionalProperties": false + } + }, + { + "name": "getBizMonitorBoard", + "inputSchema": { + "properties": { + "batch_id": { + "type": "string", + "description": "割接批次 ID" + }, + "run_id": { + "type": "string", + "description": "可选评估运行 ID" + } + }, + "required": [ + "batch_id" + ], + "type": "object", + "additionalProperties": false + } + }, + { + "name": "listBizMonitorReds", + "inputSchema": { + "properties": { + "project_id": { + "type": "string", + "description": "" + }, + "status": { + "type": "string", + "description": "open / carried / resolved;空为全部" + }, + "limit": { + "type": "integer", + "default": 200, + "minimum": 1, + "maximum": 500 + } + }, + "required": [ + "project_id" + ], + "type": "object", + "additionalProperties": false + } + }, + { + "name": "getBizMonitorDiffs", + "inputSchema": { + "properties": { + "run_id": { + "type": "string", + "description": "" + }, + "metric_id": { + "type": "string", + "description": "" + }, + "sheet_id": { + "type": "string", + "description": "" + }, + "verdict": { + "type": "string", + "description": "" + }, + "color": { + "type": "string", + "description": "如 red" + }, + "q": { + "type": "string", + "description": "差异关键词,映射 HTTP kw" + }, + "limit": { + "type": "integer", + "default": 100, + "minimum": 1, + "maximum": 500 + }, + "offset": { + "type": "integer", + "default": 0, + "minimum": 0, + "maximum": 9007199254740991 + } + }, + "required": [ + "run_id" + ], + "type": "object", + "additionalProperties": false + } + }, + { + "name": "getBizCollectBatch", + "inputSchema": { + "properties": { + "batch_id": { + "type": "string", + "description": "业务状态采集批次 ID" + } + }, + "required": [ + "batch_id" + ], + "type": "object", + "additionalProperties": false + } + }, + { + "name": "getBizCollectCommandRaw", + "inputSchema": { + "properties": { + "batch_id": { + "type": "string", + "description": "采集批次 ID" + }, + "command_id": { + "type": "string", + "description": "getBizCollectBatch 返回的命令 ID" + } + }, + "required": [ + "batch_id", + "command_id" + ], + "type": "object", + "additionalProperties": false + } + } +] \ No newline at end of file diff --git a/test/mcp-schema.test.mjs b/test/mcp-schema.test.mjs new file mode 100644 index 0000000..2a835c2 --- /dev/null +++ b/test/mcp-schema.test.mjs @@ -0,0 +1,51 @@ +import assert from 'node:assert/strict' +import { readFileSync } from 'node:fs' +import test from 'node:test' +import { defineTool } from '@deepseek-ai/dsh-tools' +import { parametersFromInputSchema } from '../src/netx/mcp-schema.ts' + +const tools = JSON.parse(readFileSync(new URL('./fixtures-netx-mcp-tools.json', import.meta.url), 'utf8')) + +function define(name, inputSchema) { + return defineTool({ + name: `netx__${name}`, + description: name, + parameters: parametersFromInputSchema(inputSchema), + output: { schema: { type: 'object', additionalProperties: true }, render: () => [] }, + async execute() { + return {} + }, + }) +} + +test('every real netx /mcp tool schema compiles in the DSH value schema DSL', () => { + assert.ok(tools.length >= 39) + for (const tool of tools) { + assert.doesNotThrow(() => define(tool.name, tool.inputSchema), tool.name) + } +}) + +test('required, constraints and nullable unions survive conversion', () => { + const params = parametersFromInputSchema({ + type: 'object', + required: ['ne_id'], + properties: { + ne_id: { type: 'string', description: 'NE id' }, + page: { type: 'integer', minimum: 1, maximum: 50, default: 1 }, + ids: { type: 'array', items: { type: 'string' }, maxItems: 20 }, + filter: { type: 'object', required: ['k'], properties: { k: { type: 'string' } } }, + maybe: { anyOf: [{ type: 'string' }, { type: 'null' }] }, + either: { type: ['string', 'integer'] }, + }, + }) + assert.equal(params.ne_id.required, true) + assert.equal(params.page.required, undefined) + assert.equal(params.page.minimum, undefined) + assert.match(params.page.description, /minimum=1, maximum=50/) + assert.match(params.ids.description, /maxItems=20/) + assert.equal(params.filter.additionalProperties, true) + assert.equal(params.filter.properties.k.required, true) + assert.equal(params.maybe.type, 'string') + assert.equal(params.either.oneOf.length, 2) + assert.doesNotThrow(() => define('sample', { type: 'object', properties: params })) +})