@calltelemetry/cucm-mcp 0.1.4 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/dime.js +12 -2
- package/dist/index.js +18 -2
- package/dist/packetCapture.js +34 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -89,3 +89,4 @@ Live tests are opt-in via env vars; see `test/live.test.js`.
|
|
|
89
89
|
|
|
90
90
|
- Use the platform/OS admin for SSH (`administrator` user on most lab systems)
|
|
91
91
|
- To request a high packet count without specifying an exact number, pass `maxPackets: true` to `packet_capture_start`
|
|
92
|
+
- If traffic is low, a small `count` can still run “forever” waiting for packets; use `packet_capture_stop` to cancel, or set `maxDurationMs` to auto-stop
|
package/dist/dime.js
CHANGED
|
@@ -27,8 +27,18 @@ function dimeXmlBytes(contentType, bytes) {
|
|
|
27
27
|
return bytes;
|
|
28
28
|
const parts = parseMultipartRelated(bytes, boundary);
|
|
29
29
|
const xmlParts = parts.filter((p) => isXmlContentType(p.contentType));
|
|
30
|
-
if (xmlParts.length === 0)
|
|
31
|
-
|
|
30
|
+
if (xmlParts.length === 0) {
|
|
31
|
+
// Some CUCM versions have been observed returning multipart bodies where the XML part
|
|
32
|
+
// does not advertise a classic XML content-type. Fall back to sniffing the payload.
|
|
33
|
+
const sniffed = parts.find((p) => {
|
|
34
|
+
const head = p.body.subarray(0, 64).toString("utf8");
|
|
35
|
+
return head.includes("<?xml") || head.trimStart().startsWith("<");
|
|
36
|
+
});
|
|
37
|
+
if (sniffed)
|
|
38
|
+
return sniffed.body;
|
|
39
|
+
const partTypes = parts.map((p) => p.contentType).join(", ");
|
|
40
|
+
throw new Error(`DIME response missing text/xml part (boundary=${boundary}; parts=[${partTypes || "none"}])`);
|
|
41
|
+
}
|
|
32
42
|
return xmlParts[0].body;
|
|
33
43
|
}
|
|
34
44
|
export function normalizeHost(hostOrUrl) {
|
package/dist/index.js
CHANGED
|
@@ -14,7 +14,7 @@ const strictTls = tlsMode === "strict" || tlsMode === "verify";
|
|
|
14
14
|
// Set CUCM_MCP_TLS_MODE=strict to enforce verification.
|
|
15
15
|
if (!strictTls)
|
|
16
16
|
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
|
|
17
|
-
const server = new McpServer({ name: "cucm", version: "0.1.
|
|
17
|
+
const server = new McpServer({ name: "cucm", version: "0.1.6" });
|
|
18
18
|
const captures = new PacketCaptureManager();
|
|
19
19
|
const captureState = defaultStateStore();
|
|
20
20
|
const dimeAuthSchema = z
|
|
@@ -114,7 +114,21 @@ server.tool("packet_capture_start", "Start a packet capture on CUCM via SSH (uti
|
|
|
114
114
|
size: z.string().optional().describe("Packet size (e.g. all)"),
|
|
115
115
|
hostFilterIp: z.string().optional().describe("Optional filter: host ip <addr>"),
|
|
116
116
|
portFilter: z.number().int().min(1).max(65535).optional().describe("Optional filter: port <num>"),
|
|
117
|
-
|
|
117
|
+
maxDurationMs: z
|
|
118
|
+
.number()
|
|
119
|
+
.int()
|
|
120
|
+
.min(250)
|
|
121
|
+
.max(24 * 60 * 60_000)
|
|
122
|
+
.optional()
|
|
123
|
+
.describe("Stop after this duration even if packet count isn't reached"),
|
|
124
|
+
startTimeoutMs: z
|
|
125
|
+
.number()
|
|
126
|
+
.int()
|
|
127
|
+
.min(2000)
|
|
128
|
+
.max(120_000)
|
|
129
|
+
.optional()
|
|
130
|
+
.describe("Timeout for starting capture (SSH connect + command start)"),
|
|
131
|
+
}, async ({ host, sshPort, auth, iface, fileBase, count, maxPackets, size, hostFilterIp, portFilter, maxDurationMs, startTimeoutMs }) => {
|
|
118
132
|
const resolvedCount = count ?? (maxPackets ? 1_000_000 : undefined);
|
|
119
133
|
const result = await captures.start({
|
|
120
134
|
host,
|
|
@@ -126,6 +140,8 @@ server.tool("packet_capture_start", "Start a packet capture on CUCM via SSH (uti
|
|
|
126
140
|
size,
|
|
127
141
|
hostFilterIp,
|
|
128
142
|
portFilter,
|
|
143
|
+
maxDurationMs,
|
|
144
|
+
startTimeoutMs,
|
|
129
145
|
});
|
|
130
146
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
131
147
|
});
|
package/dist/packetCapture.js
CHANGED
|
@@ -195,8 +195,31 @@ export class PacketCaptureManager {
|
|
|
195
195
|
}
|
|
196
196
|
});
|
|
197
197
|
// Wait for prompt before running capture command.
|
|
198
|
+
// Some CUCM shells don't print a prompt until you send a newline.
|
|
199
|
+
const promptTimeoutMs = Math.max(2000, Math.trunc(opts.startTimeoutMs ?? 30_000));
|
|
198
200
|
try {
|
|
199
|
-
|
|
201
|
+
try {
|
|
202
|
+
channel.write("\n");
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
// ignore
|
|
206
|
+
}
|
|
207
|
+
// Nudge a couple times if no prompt yet.
|
|
208
|
+
void (async () => {
|
|
209
|
+
const delays = [400, 1200, 2500];
|
|
210
|
+
for (const d of delays) {
|
|
211
|
+
await new Promise((r) => setTimeout(r, d));
|
|
212
|
+
if (looksLikeCucmPrompt(session.lastStdout) || looksLikeCucmPrompt(session.lastStderr))
|
|
213
|
+
return;
|
|
214
|
+
try {
|
|
215
|
+
channel.write("\n");
|
|
216
|
+
}
|
|
217
|
+
catch {
|
|
218
|
+
// ignore
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
})();
|
|
222
|
+
await waitFor(channel, session, () => looksLikeCucmPrompt(session.lastStdout) || looksLikeCucmPrompt(session.lastStderr), promptTimeoutMs, "Timeout waiting for CUCM CLI prompt");
|
|
200
223
|
channel.write(`${cmd}\n`);
|
|
201
224
|
}
|
|
202
225
|
catch (e) {
|
|
@@ -217,6 +240,16 @@ export class PacketCaptureManager {
|
|
|
217
240
|
throw e;
|
|
218
241
|
}
|
|
219
242
|
this.active.set(id, { session, client, channel });
|
|
243
|
+
// Optional guard: stop after a duration even if packet count isn't reached.
|
|
244
|
+
if (opts.maxDurationMs != null) {
|
|
245
|
+
const dur = Math.max(250, Math.trunc(opts.maxDurationMs));
|
|
246
|
+
const t = setTimeout(() => {
|
|
247
|
+
void this.stop(id).catch(() => {
|
|
248
|
+
// ignore
|
|
249
|
+
});
|
|
250
|
+
}, dur);
|
|
251
|
+
t.unref?.();
|
|
252
|
+
}
|
|
220
253
|
return session;
|
|
221
254
|
}
|
|
222
255
|
async stop(captureId, timeoutMs = 90_000) {
|