ilya 1.0.0 → 1.0.1
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/package.json +1 -1
- package/dist/cli.d.ts +0 -4
- package/dist/cli.js +0 -85
- package/dist/colors.d.ts +0 -28
- package/dist/colors.js +0 -68
- package/dist/colors.test.d.ts +0 -1
- package/dist/colors.test.js +0 -48
- package/dist/formatter.d.ts +0 -18
- package/dist/formatter.js +0 -95
- package/dist/formatter.test.d.ts +0 -1
- package/dist/formatter.test.js +0 -100
- package/dist/http-server.d.ts +0 -9
- package/dist/http-server.js +0 -26
- package/dist/line-buffer.d.ts +0 -7
- package/dist/line-buffer.js +0 -20
- package/dist/line-buffer.test.d.ts +0 -1
- package/dist/line-buffer.test.js +0 -44
- package/dist/logger.d.ts +0 -16
- package/dist/logger.js +0 -51
- package/dist/proxy.d.ts +0 -16
- package/dist/proxy.js +0 -100
- package/dist/result-formatter.d.ts +0 -22
- package/dist/result-formatter.js +0 -90
- package/dist/result-formatter.test.d.ts +0 -1
- package/dist/result-formatter.test.js +0 -99
- package/dist/utils.d.ts +0 -12
- package/dist/utils.js +0 -23
- package/dist/utils.test.d.ts +0 -1
- package/dist/utils.test.js +0 -28
- /package/bin/{mcp-tap.js → ilya.js} +0 -0
package/package.json
CHANGED
package/dist/cli.d.ts
DELETED
package/dist/cli.js
DELETED
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
import { createColors, PLAIN } from "./colors.js";
|
|
2
|
-
import { Logger } from "./logger.js";
|
|
3
|
-
import { startHttpServer } from "./http-server.js";
|
|
4
|
-
import { startProxy } from "./proxy.js";
|
|
5
|
-
/**
|
|
6
|
-
* Print the usage information for the CLI.
|
|
7
|
-
*/
|
|
8
|
-
const printUsage = () => {
|
|
9
|
-
process.stderr.write(`serde — transparent MCP stdio proxy with logging
|
|
10
|
-
|
|
11
|
-
Usage:
|
|
12
|
-
serde [options] <command> [args...]
|
|
13
|
-
|
|
14
|
-
Options:
|
|
15
|
-
--log, -l <path> Write logs to a specific file
|
|
16
|
-
--port, -p <port> Start an HTTP server to stream logs
|
|
17
|
-
--help, -h Show this help
|
|
18
|
-
--version, -v Show version
|
|
19
|
-
|
|
20
|
-
Examples:
|
|
21
|
-
serde node ./my-server.js
|
|
22
|
-
serde --log /tmp/tap.log python server.py
|
|
23
|
-
serde --port 3456 npx ts-node ./server.ts
|
|
24
|
-
`);
|
|
25
|
-
};
|
|
26
|
-
/**
|
|
27
|
-
* Parse command-line arguments into a structured format.
|
|
28
|
-
* @param argv The command-line arguments to parse
|
|
29
|
-
*/
|
|
30
|
-
const parseArgs = (argv) => {
|
|
31
|
-
const args = argv.slice(2);
|
|
32
|
-
let logFilePath = null;
|
|
33
|
-
let httpPort = null;
|
|
34
|
-
let serverCmd = null;
|
|
35
|
-
let serverArgs = [];
|
|
36
|
-
for (let i = 0; i < args.length; i++) {
|
|
37
|
-
if ((args[i] === "--log" || args[i] === "-l") && i + 1 < args.length) {
|
|
38
|
-
logFilePath = args[++i];
|
|
39
|
-
}
|
|
40
|
-
else if ((args[i] === "--port" || args[i] === "-p") &&
|
|
41
|
-
i + 1 < args.length) {
|
|
42
|
-
httpPort = parseInt(args[++i], 10);
|
|
43
|
-
}
|
|
44
|
-
else if (args[i] === "--help" || args[i] === "-h") {
|
|
45
|
-
printUsage();
|
|
46
|
-
process.exit(0);
|
|
47
|
-
}
|
|
48
|
-
else if (args[i] === "--version" || args[i] === "-v") {
|
|
49
|
-
process.stderr.write("serde 0.1.0\n");
|
|
50
|
-
process.exit(0);
|
|
51
|
-
}
|
|
52
|
-
else {
|
|
53
|
-
serverCmd = args[i];
|
|
54
|
-
serverArgs = args.slice(i + 1);
|
|
55
|
-
break;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
return { logFilePath, httpPort, serverCmd, serverArgs };
|
|
59
|
-
};
|
|
60
|
-
/**
|
|
61
|
-
* Run the CLI application.
|
|
62
|
-
*/
|
|
63
|
-
export const run = () => {
|
|
64
|
-
const { logFilePath, httpPort, serverCmd, serverArgs } = parseArgs(process.argv);
|
|
65
|
-
if (!serverCmd) {
|
|
66
|
-
printUsage();
|
|
67
|
-
process.exit(1);
|
|
68
|
-
return; // unreachable, helps TS narrow
|
|
69
|
-
}
|
|
70
|
-
const isTTY = !!process.stderr.isTTY;
|
|
71
|
-
const colors = createColors(isTTY);
|
|
72
|
-
const logger = new Logger({ logFilePath, isTTY, serverCmd });
|
|
73
|
-
const pendingRequests = new Map();
|
|
74
|
-
if (httpPort) {
|
|
75
|
-
startHttpServer(httpPort, logger, `${serverCmd} ${serverArgs.join(" ")}`);
|
|
76
|
-
}
|
|
77
|
-
startProxy({
|
|
78
|
-
serverCmd,
|
|
79
|
-
serverArgs,
|
|
80
|
-
logger,
|
|
81
|
-
colors,
|
|
82
|
-
ctx: { logger, colors, plainColors: PLAIN, pendingRequests },
|
|
83
|
-
});
|
|
84
|
-
};
|
|
85
|
-
run();
|
package/dist/colors.d.ts
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
export interface Colors {
|
|
2
|
-
RESET: string;
|
|
3
|
-
BOLD: string;
|
|
4
|
-
DIM: string;
|
|
5
|
-
RED: string;
|
|
6
|
-
GREEN: string;
|
|
7
|
-
YELLOW: string;
|
|
8
|
-
BLUE: string;
|
|
9
|
-
MAGENTA: string;
|
|
10
|
-
CYAN: string;
|
|
11
|
-
WHITE: string;
|
|
12
|
-
}
|
|
13
|
-
/**
|
|
14
|
-
* Create a Colors object with ANSI escape codes if the terminal supports it.
|
|
15
|
-
*
|
|
16
|
-
* @param isTTY Whether the output is a TTY (supports colors)
|
|
17
|
-
* @returns A Colors object with appropriate escape codes or empty strings
|
|
18
|
-
*/
|
|
19
|
-
export declare const createColors: (isTTY: boolean) => Colors;
|
|
20
|
-
export declare const PLAIN: Colors;
|
|
21
|
-
/**
|
|
22
|
-
* Get the color for a given method name.
|
|
23
|
-
*
|
|
24
|
-
* @param method The method name
|
|
25
|
-
* @param colors The Colors object to use
|
|
26
|
-
* @returns The color string for the method
|
|
27
|
-
*/
|
|
28
|
-
export declare const methodColor: (method: string | undefined, colors: Colors) => string;
|
package/dist/colors.js
DELETED
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Create a Colors object with ANSI escape codes if the terminal supports it.
|
|
3
|
-
*
|
|
4
|
-
* @param isTTY Whether the output is a TTY (supports colors)
|
|
5
|
-
* @returns A Colors object with appropriate escape codes or empty strings
|
|
6
|
-
*/
|
|
7
|
-
export const createColors = (isTTY) => {
|
|
8
|
-
if (isTTY) {
|
|
9
|
-
return {
|
|
10
|
-
RESET: "\x1b[0m",
|
|
11
|
-
BOLD: "\x1b[1m",
|
|
12
|
-
DIM: "\x1b[2m",
|
|
13
|
-
RED: "\x1b[31m",
|
|
14
|
-
GREEN: "\x1b[32m",
|
|
15
|
-
YELLOW: "\x1b[33m",
|
|
16
|
-
BLUE: "\x1b[34m",
|
|
17
|
-
MAGENTA: "\x1b[35m",
|
|
18
|
-
CYAN: "\x1b[36m",
|
|
19
|
-
WHITE: "\x1b[37m",
|
|
20
|
-
};
|
|
21
|
-
}
|
|
22
|
-
return {
|
|
23
|
-
RESET: "",
|
|
24
|
-
BOLD: "",
|
|
25
|
-
DIM: "",
|
|
26
|
-
RED: "",
|
|
27
|
-
GREEN: "",
|
|
28
|
-
YELLOW: "",
|
|
29
|
-
BLUE: "",
|
|
30
|
-
MAGENTA: "",
|
|
31
|
-
CYAN: "",
|
|
32
|
-
WHITE: "",
|
|
33
|
-
};
|
|
34
|
-
};
|
|
35
|
-
export const PLAIN = {
|
|
36
|
-
RESET: "",
|
|
37
|
-
BOLD: "",
|
|
38
|
-
DIM: "",
|
|
39
|
-
RED: "",
|
|
40
|
-
GREEN: "",
|
|
41
|
-
YELLOW: "",
|
|
42
|
-
BLUE: "",
|
|
43
|
-
MAGENTA: "",
|
|
44
|
-
CYAN: "",
|
|
45
|
-
WHITE: "",
|
|
46
|
-
};
|
|
47
|
-
/**
|
|
48
|
-
* Get the color for a given method name.
|
|
49
|
-
*
|
|
50
|
-
* @param method The method name
|
|
51
|
-
* @param colors The Colors object to use
|
|
52
|
-
* @returns The color string for the method
|
|
53
|
-
*/
|
|
54
|
-
export const methodColor = (method, colors) => {
|
|
55
|
-
if (!method)
|
|
56
|
-
return colors.WHITE;
|
|
57
|
-
if (method.startsWith("initialize"))
|
|
58
|
-
return colors.MAGENTA;
|
|
59
|
-
if (method.startsWith("tools/"))
|
|
60
|
-
return colors.GREEN;
|
|
61
|
-
if (method.startsWith("resources/"))
|
|
62
|
-
return colors.CYAN;
|
|
63
|
-
if (method.startsWith("prompts/"))
|
|
64
|
-
return colors.YELLOW;
|
|
65
|
-
if (method.startsWith("notifications/"))
|
|
66
|
-
return colors.BLUE;
|
|
67
|
-
return colors.WHITE;
|
|
68
|
-
};
|
package/dist/colors.test.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
package/dist/colors.test.js
DELETED
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { createColors, PLAIN, methodColor } from "./colors.js";
|
|
3
|
-
describe("createColors", () => {
|
|
4
|
-
it("returns ANSI codes when isTTY is true", () => {
|
|
5
|
-
const c = createColors(true);
|
|
6
|
-
expect(c.RESET).toBe("\x1b[0m");
|
|
7
|
-
expect(c.RED).toBe("\x1b[31m");
|
|
8
|
-
expect(c.BOLD).toBe("\x1b[1m");
|
|
9
|
-
});
|
|
10
|
-
it("returns empty strings when isTTY is false", () => {
|
|
11
|
-
const c = createColors(false);
|
|
12
|
-
expect(c.RESET).toBe("");
|
|
13
|
-
expect(c.RED).toBe("");
|
|
14
|
-
expect(c.BOLD).toBe("");
|
|
15
|
-
});
|
|
16
|
-
});
|
|
17
|
-
describe("PLAIN", () => {
|
|
18
|
-
it("has all empty strings", () => {
|
|
19
|
-
for (const val of Object.values(PLAIN)) {
|
|
20
|
-
expect(val).toBe("");
|
|
21
|
-
}
|
|
22
|
-
});
|
|
23
|
-
});
|
|
24
|
-
describe("methodColor", () => {
|
|
25
|
-
const c = createColors(true);
|
|
26
|
-
it("returns MAGENTA for initialize", () => {
|
|
27
|
-
expect(methodColor("initialize", c)).toBe(c.MAGENTA);
|
|
28
|
-
});
|
|
29
|
-
it("returns GREEN for tools/*", () => {
|
|
30
|
-
expect(methodColor("tools/list", c)).toBe(c.GREEN);
|
|
31
|
-
expect(methodColor("tools/call", c)).toBe(c.GREEN);
|
|
32
|
-
});
|
|
33
|
-
it("returns CYAN for resources/*", () => {
|
|
34
|
-
expect(methodColor("resources/list", c)).toBe(c.CYAN);
|
|
35
|
-
});
|
|
36
|
-
it("returns YELLOW for prompts/*", () => {
|
|
37
|
-
expect(methodColor("prompts/list", c)).toBe(c.YELLOW);
|
|
38
|
-
});
|
|
39
|
-
it("returns BLUE for notifications/*", () => {
|
|
40
|
-
expect(methodColor("notifications/initialized", c)).toBe(c.BLUE);
|
|
41
|
-
});
|
|
42
|
-
it("returns WHITE for unknown methods", () => {
|
|
43
|
-
expect(methodColor("something/else", c)).toBe(c.WHITE);
|
|
44
|
-
});
|
|
45
|
-
it("returns WHITE for undefined", () => {
|
|
46
|
-
expect(methodColor(undefined, c)).toBe(c.WHITE);
|
|
47
|
-
});
|
|
48
|
-
});
|
package/dist/formatter.d.ts
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import type { Colors } from "./colors.js";
|
|
2
|
-
import type { Logger } from "./logger.js";
|
|
3
|
-
import type { PendingRequest } from "./result-formatter.js";
|
|
4
|
-
export interface MessageContext {
|
|
5
|
-
logger: Logger;
|
|
6
|
-
colors: Colors;
|
|
7
|
-
plainColors: Colors;
|
|
8
|
-
pendingRequests: Map<string | number, PendingRequest>;
|
|
9
|
-
}
|
|
10
|
-
/**
|
|
11
|
-
* Format a JSON-RPC message for logging.
|
|
12
|
-
*
|
|
13
|
-
* @param json The JSON-RPC message as a string
|
|
14
|
-
* @param direction The direction of the message, either "client" or "server"
|
|
15
|
-
* @param ctx The context for formatting, including logger, colors, and pending requests
|
|
16
|
-
* @returns void
|
|
17
|
-
*/
|
|
18
|
-
export declare const formatMessage: (json: string, direction: "client" | "server", ctx: MessageContext) => void;
|
package/dist/formatter.js
DELETED
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
import { methodColor } from "./colors.js";
|
|
2
|
-
import { formatResult } from "./result-formatter.js";
|
|
3
|
-
import { timestamp, truncate } from "./utils.js";
|
|
4
|
-
/**
|
|
5
|
-
* Format a JSON-RPC message for logging.
|
|
6
|
-
*
|
|
7
|
-
* @param json The JSON-RPC message as a string
|
|
8
|
-
* @param direction The direction of the message, either "client" or "server"
|
|
9
|
-
* @param ctx The context for formatting, including logger, colors, and pending requests
|
|
10
|
-
* @returns void
|
|
11
|
-
*/
|
|
12
|
-
export const formatMessage = (json, direction, ctx) => {
|
|
13
|
-
const { logger, colors: c, plainColors: pc, pendingRequests } = ctx;
|
|
14
|
-
const isClient = direction === "client";
|
|
15
|
-
const arrow = isClient ? "→" : "←";
|
|
16
|
-
const label = isClient ? "CLIENT" : "SERVER";
|
|
17
|
-
const dirColor = isClient ? c.CYAN : c.GREEN;
|
|
18
|
-
const ts = timestamp();
|
|
19
|
-
try {
|
|
20
|
-
const msg = JSON.parse(json);
|
|
21
|
-
const id = msg.id;
|
|
22
|
-
const method = msg.method;
|
|
23
|
-
if (method && id === undefined) {
|
|
24
|
-
const mc = methodColor(method, c);
|
|
25
|
-
const colored = `${c.DIM}${ts}${c.RESET} ${dirColor}${arrow} ${label}${c.RESET} ${c.BLUE}notification${c.RESET} ${mc}${method}${c.RESET}`;
|
|
26
|
-
const plain = `${ts} ${arrow} ${label} notification ${method}`;
|
|
27
|
-
logger.write(colored, plain);
|
|
28
|
-
const params = msg.params;
|
|
29
|
-
if (params && Object.keys(params).length > 0) {
|
|
30
|
-
const paramStr = truncate(JSON.stringify(params), 200);
|
|
31
|
-
logger.write(` ${c.DIM}${paramStr}${c.RESET}`, ` ${paramStr}`);
|
|
32
|
-
}
|
|
33
|
-
return;
|
|
34
|
-
}
|
|
35
|
-
if (method && id !== undefined) {
|
|
36
|
-
const mc = methodColor(method, c);
|
|
37
|
-
let toolName = null;
|
|
38
|
-
const params = msg.params;
|
|
39
|
-
if (method === "tools/call" && params?.name) {
|
|
40
|
-
toolName = params.name;
|
|
41
|
-
}
|
|
42
|
-
pendingRequests.set(id, { method, toolName, ts: Date.now() });
|
|
43
|
-
const extra = toolName ? ` ${c.BOLD}${toolName}${c.RESET}` : "";
|
|
44
|
-
const extraPlain = toolName ? ` ${toolName}` : "";
|
|
45
|
-
const colored = `${c.DIM}${ts}${c.RESET} ${dirColor}${arrow} ${label}${c.RESET} ${c.WHITE}request${c.RESET} ${mc}${method}${c.RESET}${extra} ${c.DIM}#${id}${c.RESET}`;
|
|
46
|
-
const plain = `${ts} ${arrow} ${label} request ${method}${extraPlain} #${id}`;
|
|
47
|
-
logger.write(colored, plain);
|
|
48
|
-
if (method === "tools/call" && params?.arguments) {
|
|
49
|
-
const argStr = JSON.stringify(params.arguments, null, 2);
|
|
50
|
-
const lines = argStr.split("\n");
|
|
51
|
-
for (const line of lines) {
|
|
52
|
-
logger.write(` ${c.DIM}${line}${c.RESET}`, ` ${line}`);
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
else if (params && Object.keys(params).length > 0) {
|
|
56
|
-
const paramStr = truncate(JSON.stringify(params), 200);
|
|
57
|
-
logger.write(` ${c.DIM}${paramStr}${c.RESET}`, ` ${paramStr}`);
|
|
58
|
-
}
|
|
59
|
-
return;
|
|
60
|
-
}
|
|
61
|
-
if (id !== undefined && !method) {
|
|
62
|
-
const pending = pendingRequests.get(id);
|
|
63
|
-
const reqMethod = pending?.method || "unknown";
|
|
64
|
-
const mc = methodColor(reqMethod, c);
|
|
65
|
-
const elapsed = pending ? `${Date.now() - pending.ts}ms` : "";
|
|
66
|
-
const error = msg.error;
|
|
67
|
-
if (error) {
|
|
68
|
-
const code = error.code || "?";
|
|
69
|
-
const errMsg = error.message || "Unknown error";
|
|
70
|
-
const colored = `${c.DIM}${ts}${c.RESET} ${dirColor}${arrow} ${label}${c.RESET} ${c.RED}${c.BOLD}ERROR${c.RESET} ${mc}(${reqMethod} #${id})${c.RESET} ${c.DIM}${elapsed}${c.RESET}`;
|
|
71
|
-
const plain = `${ts} ${arrow} ${label} ERROR (${reqMethod} #${id}) ${elapsed}`;
|
|
72
|
-
logger.write(colored, plain);
|
|
73
|
-
logger.write(` ${c.RED}[${code}] ${errMsg}${c.RESET}`, ` [${code}] ${errMsg}`);
|
|
74
|
-
pendingRequests.delete(id);
|
|
75
|
-
return;
|
|
76
|
-
}
|
|
77
|
-
const colored = `${c.DIM}${ts}${c.RESET} ${dirColor}${arrow} ${label}${c.RESET} ${c.WHITE}response${c.RESET} ${mc}${reqMethod}${c.RESET} ${c.DIM}#${id} ${elapsed}${c.RESET}`;
|
|
78
|
-
const plain = `${ts} ${arrow} ${label} response ${reqMethod} #${id} ${elapsed}`;
|
|
79
|
-
logger.write(colored, plain);
|
|
80
|
-
formatResult(reqMethod, msg.result, pending, {
|
|
81
|
-
logger,
|
|
82
|
-
colors: c,
|
|
83
|
-
plainColors: pc,
|
|
84
|
-
});
|
|
85
|
-
pendingRequests.delete(id);
|
|
86
|
-
return;
|
|
87
|
-
}
|
|
88
|
-
const raw = truncate(json.trim(), 200);
|
|
89
|
-
logger.write(`${c.DIM}${ts}${c.RESET} ${dirColor}${arrow} ${label}${c.RESET} ${c.DIM}${raw}${c.RESET}`, `${ts} ${arrow} ${label} ${raw}`);
|
|
90
|
-
}
|
|
91
|
-
catch {
|
|
92
|
-
const raw = truncate(json.trim(), 200);
|
|
93
|
-
logger.write(`${c.DIM}${ts}${c.RESET} ${dirColor}${arrow} ${label}${c.RESET} ${c.YELLOW}[raw]${c.RESET} ${raw}`, `${ts} ${arrow} ${label} [raw] ${raw}`);
|
|
94
|
-
}
|
|
95
|
-
};
|
package/dist/formatter.test.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
package/dist/formatter.test.js
DELETED
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi } from "vitest";
|
|
2
|
-
import { formatMessage } from "./formatter.js";
|
|
3
|
-
import { PLAIN } from "./colors.js";
|
|
4
|
-
function createMockCtx() {
|
|
5
|
-
const written = [];
|
|
6
|
-
const pendingRequests = new Map();
|
|
7
|
-
const logger = {
|
|
8
|
-
write: vi.fn((colored, plain) => {
|
|
9
|
-
written.push({ colored, plain });
|
|
10
|
-
}),
|
|
11
|
-
};
|
|
12
|
-
const ctx = {
|
|
13
|
-
logger: logger,
|
|
14
|
-
colors: PLAIN,
|
|
15
|
-
plainColors: PLAIN,
|
|
16
|
-
pendingRequests,
|
|
17
|
-
};
|
|
18
|
-
return { ctx, written, pendingRequests };
|
|
19
|
-
}
|
|
20
|
-
describe("formatMessage", () => {
|
|
21
|
-
it("formats a notification (method, no id)", () => {
|
|
22
|
-
const { ctx, written } = createMockCtx();
|
|
23
|
-
const json = JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" });
|
|
24
|
-
formatMessage(json, "client", ctx);
|
|
25
|
-
expect(written[0].plain).toContain("CLIENT");
|
|
26
|
-
expect(written[0].plain).toContain("notification");
|
|
27
|
-
expect(written[0].plain).toContain("notifications/initialized");
|
|
28
|
-
});
|
|
29
|
-
it("formats a request (method + id)", () => {
|
|
30
|
-
const { ctx, written, pendingRequests } = createMockCtx();
|
|
31
|
-
const json = JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/list" });
|
|
32
|
-
formatMessage(json, "client", ctx);
|
|
33
|
-
expect(written[0].plain).toContain("request");
|
|
34
|
-
expect(written[0].plain).toContain("tools/list");
|
|
35
|
-
expect(written[0].plain).toContain("#1");
|
|
36
|
-
expect(pendingRequests.has(1)).toBe(true);
|
|
37
|
-
});
|
|
38
|
-
it("formats a tools/call request with tool name", () => {
|
|
39
|
-
const { ctx, written } = createMockCtx();
|
|
40
|
-
const json = JSON.stringify({
|
|
41
|
-
jsonrpc: "2.0", id: 5, method: "tools/call",
|
|
42
|
-
params: { name: "echo", arguments: { msg: "hi" } },
|
|
43
|
-
});
|
|
44
|
-
formatMessage(json, "client", ctx);
|
|
45
|
-
expect(written[0].plain).toContain("echo");
|
|
46
|
-
expect(written[0].plain).toContain("#5");
|
|
47
|
-
// arguments are printed on subsequent lines
|
|
48
|
-
expect(written.length).toBeGreaterThan(1);
|
|
49
|
-
});
|
|
50
|
-
it("formats a success response", () => {
|
|
51
|
-
const { ctx, written, pendingRequests } = createMockCtx();
|
|
52
|
-
pendingRequests.set(1, { method: "tools/list", toolName: null, ts: Date.now() - 50 });
|
|
53
|
-
const json = JSON.stringify({
|
|
54
|
-
jsonrpc: "2.0", id: 1,
|
|
55
|
-
result: { tools: [{ name: "echo" }] },
|
|
56
|
-
});
|
|
57
|
-
formatMessage(json, "server", ctx);
|
|
58
|
-
expect(written[0].plain).toContain("response");
|
|
59
|
-
expect(written[0].plain).toContain("tools/list");
|
|
60
|
-
expect(pendingRequests.has(1)).toBe(false);
|
|
61
|
-
});
|
|
62
|
-
it("formats an error response", () => {
|
|
63
|
-
const { ctx, written, pendingRequests } = createMockCtx();
|
|
64
|
-
pendingRequests.set(2, { method: "tools/call", toolName: "fail", ts: Date.now() });
|
|
65
|
-
const json = JSON.stringify({
|
|
66
|
-
jsonrpc: "2.0", id: 2,
|
|
67
|
-
error: { code: -32600, message: "Invalid request" },
|
|
68
|
-
});
|
|
69
|
-
formatMessage(json, "server", ctx);
|
|
70
|
-
expect(written[0].plain).toContain("ERROR");
|
|
71
|
-
expect(written[1].plain).toContain("-32600");
|
|
72
|
-
expect(written[1].plain).toContain("Invalid request");
|
|
73
|
-
});
|
|
74
|
-
it("handles invalid JSON gracefully", () => {
|
|
75
|
-
const { ctx, written } = createMockCtx();
|
|
76
|
-
formatMessage("not json at all", "client", ctx);
|
|
77
|
-
expect(written[0].plain).toContain("[raw]");
|
|
78
|
-
expect(written[0].plain).toContain("not json at all");
|
|
79
|
-
});
|
|
80
|
-
it("uses correct direction labels", () => {
|
|
81
|
-
const { ctx, written } = createMockCtx();
|
|
82
|
-
const json = JSON.stringify({ jsonrpc: "2.0", method: "notifications/test" });
|
|
83
|
-
formatMessage(json, "client", ctx);
|
|
84
|
-
expect(written[0].plain).toContain("→");
|
|
85
|
-
expect(written[0].plain).toContain("CLIENT");
|
|
86
|
-
formatMessage(json, "server", ctx);
|
|
87
|
-
expect(written[1].plain).toContain("←");
|
|
88
|
-
expect(written[1].plain).toContain("SERVER");
|
|
89
|
-
});
|
|
90
|
-
it("shows notification params when present", () => {
|
|
91
|
-
const { ctx, written } = createMockCtx();
|
|
92
|
-
const json = JSON.stringify({
|
|
93
|
-
jsonrpc: "2.0", method: "notifications/progress",
|
|
94
|
-
params: { token: "abc", progress: 50 },
|
|
95
|
-
});
|
|
96
|
-
formatMessage(json, "server", ctx);
|
|
97
|
-
expect(written.length).toBe(2);
|
|
98
|
-
expect(written[1].plain).toContain("token");
|
|
99
|
-
});
|
|
100
|
-
});
|
package/dist/http-server.d.ts
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import type { Logger } from "./logger.js";
|
|
2
|
-
/**
|
|
3
|
-
* Start an HTTP server to stream logs.
|
|
4
|
-
*
|
|
5
|
-
* @param port The port to listen on
|
|
6
|
-
* @param logger The logger to stream logs from
|
|
7
|
-
* @param serverDescription A description of the server being logged
|
|
8
|
-
*/
|
|
9
|
-
export declare const startHttpServer: (port: number, logger: Logger, serverDescription: string) => void;
|
package/dist/http-server.js
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import { createServer, } from "node:http";
|
|
2
|
-
/**
|
|
3
|
-
* Start an HTTP server to stream logs.
|
|
4
|
-
*
|
|
5
|
-
* @param port The port to listen on
|
|
6
|
-
* @param logger The logger to stream logs from
|
|
7
|
-
* @param serverDescription A description of the server being logged
|
|
8
|
-
*/
|
|
9
|
-
export const startHttpServer = (port, logger, serverDescription) => {
|
|
10
|
-
const server = createServer((req, res) => {
|
|
11
|
-
res.writeHead(200, {
|
|
12
|
-
"Content-Type": "text/plain; charset=utf-8",
|
|
13
|
-
"Cache-Control": "no-cache",
|
|
14
|
-
Connection: "keep-alive",
|
|
15
|
-
});
|
|
16
|
-
res.write(`[serde] streaming logs for: ${serverDescription}\n\n`);
|
|
17
|
-
logger.addHttpClient(res);
|
|
18
|
-
req.on("close", () => logger.removeHttpClient(res));
|
|
19
|
-
});
|
|
20
|
-
server.listen(port, "127.0.0.1", () => {
|
|
21
|
-
process.stderr.write(`[serde] log server listening on http://127.0.0.1:${port}\n`);
|
|
22
|
-
});
|
|
23
|
-
server.on("error", (err) => {
|
|
24
|
-
process.stderr.write(`[serde] failed to start HTTP server: ${err.message}\n`);
|
|
25
|
-
});
|
|
26
|
-
};
|
package/dist/line-buffer.d.ts
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Create a line buffer that calls a callback for each complete line.
|
|
3
|
-
*
|
|
4
|
-
* @param onLine The callback to call for each complete line
|
|
5
|
-
* @returns A function that can be called with chunks of text
|
|
6
|
-
*/
|
|
7
|
-
export declare const createLineBuffer: (onLine: (line: string) => void) => ((chunk: string) => void);
|
package/dist/line-buffer.js
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Create a line buffer that calls a callback for each complete line.
|
|
3
|
-
*
|
|
4
|
-
* @param onLine The callback to call for each complete line
|
|
5
|
-
* @returns A function that can be called with chunks of text
|
|
6
|
-
*/
|
|
7
|
-
export const createLineBuffer = (onLine) => {
|
|
8
|
-
let buffer = "";
|
|
9
|
-
return (chunk) => {
|
|
10
|
-
buffer += chunk;
|
|
11
|
-
let newlineIdx;
|
|
12
|
-
while ((newlineIdx = buffer.indexOf("\n")) !== -1) {
|
|
13
|
-
const line = buffer.slice(0, newlineIdx);
|
|
14
|
-
buffer = buffer.slice(newlineIdx + 1);
|
|
15
|
-
if (line.trim().length > 0) {
|
|
16
|
-
onLine(line);
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
};
|
|
20
|
-
};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
package/dist/line-buffer.test.js
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { createLineBuffer } from "./line-buffer.js";
|
|
3
|
-
describe("createLineBuffer", () => {
|
|
4
|
-
it("emits complete lines", () => {
|
|
5
|
-
const lines = [];
|
|
6
|
-
const feed = createLineBuffer((line) => lines.push(line));
|
|
7
|
-
feed("hello\nworld\n");
|
|
8
|
-
expect(lines).toEqual(["hello", "world"]);
|
|
9
|
-
});
|
|
10
|
-
it("buffers partial lines until newline arrives", () => {
|
|
11
|
-
const lines = [];
|
|
12
|
-
const feed = createLineBuffer((line) => lines.push(line));
|
|
13
|
-
feed("hel");
|
|
14
|
-
expect(lines).toEqual([]);
|
|
15
|
-
feed("lo\n");
|
|
16
|
-
expect(lines).toEqual(["hello"]);
|
|
17
|
-
});
|
|
18
|
-
it("handles multiple chunks assembling one line", () => {
|
|
19
|
-
const lines = [];
|
|
20
|
-
const feed = createLineBuffer((line) => lines.push(line));
|
|
21
|
-
feed("a");
|
|
22
|
-
feed("b");
|
|
23
|
-
feed("c\n");
|
|
24
|
-
expect(lines).toEqual(["abc"]);
|
|
25
|
-
});
|
|
26
|
-
it("skips empty lines", () => {
|
|
27
|
-
const lines = [];
|
|
28
|
-
const feed = createLineBuffer((line) => lines.push(line));
|
|
29
|
-
feed("one\n\n\ntwo\n");
|
|
30
|
-
expect(lines).toEqual(["one", "two"]);
|
|
31
|
-
});
|
|
32
|
-
it("skips whitespace-only lines", () => {
|
|
33
|
-
const lines = [];
|
|
34
|
-
const feed = createLineBuffer((line) => lines.push(line));
|
|
35
|
-
feed("one\n \ntwo\n");
|
|
36
|
-
expect(lines).toEqual(["one", "two"]);
|
|
37
|
-
});
|
|
38
|
-
it("handles multiple messages in one chunk", () => {
|
|
39
|
-
const lines = [];
|
|
40
|
-
const feed = createLineBuffer((line) => lines.push(line));
|
|
41
|
-
feed('{"a":1}\n{"b":2}\n{"c":3}\n');
|
|
42
|
-
expect(lines).toEqual(['{"a":1}', '{"b":2}', '{"c":3}']);
|
|
43
|
-
});
|
|
44
|
-
});
|
package/dist/logger.d.ts
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import type { ServerResponse } from "node:http";
|
|
2
|
-
export declare class Logger {
|
|
3
|
-
private fileStream;
|
|
4
|
-
private httpClients;
|
|
5
|
-
private isTTY;
|
|
6
|
-
readonly logFilePath: string;
|
|
7
|
-
constructor(opts: {
|
|
8
|
-
logFilePath?: string | null;
|
|
9
|
-
isTTY: boolean;
|
|
10
|
-
serverCmd: string;
|
|
11
|
-
});
|
|
12
|
-
write(coloredLine: string, plainLine: string): void;
|
|
13
|
-
addHttpClient(res: ServerResponse): void;
|
|
14
|
-
removeHttpClient(res: ServerResponse): void;
|
|
15
|
-
close(): void;
|
|
16
|
-
}
|
package/dist/logger.js
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import { createWriteStream, mkdirSync } from "node:fs";
|
|
2
|
-
import { homedir } from "node:os";
|
|
3
|
-
import { join, basename } from "node:path";
|
|
4
|
-
export class Logger {
|
|
5
|
-
fileStream;
|
|
6
|
-
httpClients = new Set();
|
|
7
|
-
isTTY;
|
|
8
|
-
logFilePath;
|
|
9
|
-
constructor(opts) {
|
|
10
|
-
this.isTTY = opts.isTTY;
|
|
11
|
-
if (opts.logFilePath) {
|
|
12
|
-
this.logFilePath = opts.logFilePath;
|
|
13
|
-
}
|
|
14
|
-
else {
|
|
15
|
-
const serverName = basename(opts.serverCmd).replace(/\.[^.]+$/, "");
|
|
16
|
-
const logDir = join(homedir(), ".serde", "logs");
|
|
17
|
-
try {
|
|
18
|
-
mkdirSync(logDir, { recursive: true });
|
|
19
|
-
}
|
|
20
|
-
catch {
|
|
21
|
-
/* ignore */
|
|
22
|
-
}
|
|
23
|
-
this.logFilePath = join(logDir, `${serverName}-${process.pid}.log`);
|
|
24
|
-
}
|
|
25
|
-
this.fileStream = createWriteStream(this.logFilePath, { flags: "a" });
|
|
26
|
-
process.stderr.write(`[serde] logging to ${this.logFilePath}\n`);
|
|
27
|
-
}
|
|
28
|
-
write(coloredLine, plainLine) {
|
|
29
|
-
if (this.isTTY) {
|
|
30
|
-
process.stderr.write(coloredLine + "\n");
|
|
31
|
-
}
|
|
32
|
-
this.fileStream.write(plainLine + "\n");
|
|
33
|
-
for (const res of this.httpClients) {
|
|
34
|
-
try {
|
|
35
|
-
res.write(plainLine + "\n");
|
|
36
|
-
}
|
|
37
|
-
catch {
|
|
38
|
-
this.httpClients.delete(res);
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
addHttpClient(res) {
|
|
43
|
-
this.httpClients.add(res);
|
|
44
|
-
}
|
|
45
|
-
removeHttpClient(res) {
|
|
46
|
-
this.httpClients.delete(res);
|
|
47
|
-
}
|
|
48
|
-
close() {
|
|
49
|
-
this.fileStream.end();
|
|
50
|
-
}
|
|
51
|
-
}
|
package/dist/proxy.d.ts
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import type { Colors } from "./colors.js";
|
|
2
|
-
import { type MessageContext } from "./formatter.js";
|
|
3
|
-
import type { Logger } from "./logger.js";
|
|
4
|
-
export interface ProxyOptions {
|
|
5
|
-
serverCmd: string;
|
|
6
|
-
serverArgs: string[];
|
|
7
|
-
logger: Logger;
|
|
8
|
-
colors: Colors;
|
|
9
|
-
ctx: MessageContext;
|
|
10
|
-
}
|
|
11
|
-
/**
|
|
12
|
-
* Start a proxy between the client and the server, logging all messages.
|
|
13
|
-
*
|
|
14
|
-
* @param opts The options for the proxy
|
|
15
|
-
*/
|
|
16
|
-
export declare const startProxy: (opts: ProxyOptions) => void;
|
package/dist/proxy.js
DELETED
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
2
|
-
import { createLineBuffer } from "./line-buffer.js";
|
|
3
|
-
import { formatMessage } from "./formatter.js";
|
|
4
|
-
/**
|
|
5
|
-
* Start a proxy between the client and the server, logging all messages.
|
|
6
|
-
*
|
|
7
|
-
* @param opts The options for the proxy
|
|
8
|
-
*/
|
|
9
|
-
export const startProxy = (opts) => {
|
|
10
|
-
const { serverCmd, serverArgs, logger, colors: c, ctx } = opts;
|
|
11
|
-
const child = spawn(serverCmd, serverArgs, {
|
|
12
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
13
|
-
env: process.env,
|
|
14
|
-
});
|
|
15
|
-
child.on("error", (err) => {
|
|
16
|
-
const msg = `[serde] failed to start server: ${err.message}`;
|
|
17
|
-
logger.write(`${c.RED}${msg}${c.RESET}`, msg);
|
|
18
|
-
process.exit(1);
|
|
19
|
-
});
|
|
20
|
-
const clientLineBuffer = createLineBuffer((line) => {
|
|
21
|
-
formatMessage(line, "client", ctx);
|
|
22
|
-
child.stdin.write(line + "\n");
|
|
23
|
-
});
|
|
24
|
-
process.stdin.on("data", (chunk) => {
|
|
25
|
-
clientLineBuffer(chunk.toString("utf-8"));
|
|
26
|
-
});
|
|
27
|
-
process.stdin.on("end", () => {
|
|
28
|
-
try {
|
|
29
|
-
child.stdin.end();
|
|
30
|
-
}
|
|
31
|
-
catch {
|
|
32
|
-
// no-op
|
|
33
|
-
}
|
|
34
|
-
});
|
|
35
|
-
const serverLineBuffer = createLineBuffer((line) => {
|
|
36
|
-
formatMessage(line, "server", ctx);
|
|
37
|
-
process.stdout.write(line + "\n");
|
|
38
|
-
});
|
|
39
|
-
child.stdout.on("data", (chunk) => {
|
|
40
|
-
serverLineBuffer(chunk.toString("utf-8"));
|
|
41
|
-
});
|
|
42
|
-
child.stderr.on("data", (chunk) => {
|
|
43
|
-
const lines = chunk.toString("utf-8").split("\n");
|
|
44
|
-
for (const line of lines) {
|
|
45
|
-
if (line.trim().length === 0)
|
|
46
|
-
continue;
|
|
47
|
-
const colored = `${c.DIM}[server stderr]${c.RESET} ${line}`;
|
|
48
|
-
const plain = `[server stderr] ${line}`;
|
|
49
|
-
logger.write(colored, plain);
|
|
50
|
-
}
|
|
51
|
-
});
|
|
52
|
-
child.on("exit", (code, signal) => {
|
|
53
|
-
const exitCode = code ?? (signal ? 1 : 0);
|
|
54
|
-
const msg = signal
|
|
55
|
-
? `[serde] server exited with signal ${signal}`
|
|
56
|
-
: `[serde] server exited with code ${exitCode}`;
|
|
57
|
-
logger.write(`${c.DIM}${msg}${c.RESET}`, msg);
|
|
58
|
-
logger.close();
|
|
59
|
-
process.exit(exitCode);
|
|
60
|
-
});
|
|
61
|
-
/**
|
|
62
|
-
* Shutdown the proxy and the child server process.
|
|
63
|
-
*
|
|
64
|
-
* @param sig The signal that triggered the shutdown
|
|
65
|
-
*/
|
|
66
|
-
const shutdown = (sig) => {
|
|
67
|
-
const msg = `[serde] received ${sig}, shutting down`;
|
|
68
|
-
logger.write(`${c.DIM}${msg}${c.RESET}`, msg);
|
|
69
|
-
try {
|
|
70
|
-
child.kill(sig);
|
|
71
|
-
}
|
|
72
|
-
catch {
|
|
73
|
-
// no-op
|
|
74
|
-
}
|
|
75
|
-
setTimeout(() => {
|
|
76
|
-
try {
|
|
77
|
-
child.kill("SIGKILL");
|
|
78
|
-
}
|
|
79
|
-
catch {
|
|
80
|
-
// no-op
|
|
81
|
-
}
|
|
82
|
-
logger.close();
|
|
83
|
-
process.exit(1);
|
|
84
|
-
}, 3000);
|
|
85
|
-
};
|
|
86
|
-
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
87
|
-
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
88
|
-
process.stdout.on("error", (err) => {
|
|
89
|
-
if (err.code === "EPIPE") {
|
|
90
|
-
const msg = "[serde] client disconnected (EPIPE)";
|
|
91
|
-
logger.write(`${c.DIM}${msg}${c.RESET}`, msg);
|
|
92
|
-
try {
|
|
93
|
-
child.kill("SIGTERM");
|
|
94
|
-
}
|
|
95
|
-
catch {
|
|
96
|
-
// no-op
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
});
|
|
100
|
-
};
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import type { Colors } from "./colors.js";
|
|
2
|
-
import type { Logger } from "./logger.js";
|
|
3
|
-
export interface PendingRequest {
|
|
4
|
-
method: string;
|
|
5
|
-
toolName: string | null;
|
|
6
|
-
ts: number;
|
|
7
|
-
}
|
|
8
|
-
export interface FormatterContext {
|
|
9
|
-
logger: Logger;
|
|
10
|
-
colors: Colors;
|
|
11
|
-
plainColors: Colors;
|
|
12
|
-
}
|
|
13
|
-
/**
|
|
14
|
-
* Format the result of a request and log it.
|
|
15
|
-
*
|
|
16
|
-
* @param method The method of the request
|
|
17
|
-
* @param result The result of the request
|
|
18
|
-
* @param _pending The pending request associated with this result
|
|
19
|
-
* @param ctx The formatter context containing the logger and colors
|
|
20
|
-
* @returns void
|
|
21
|
-
*/
|
|
22
|
-
export declare const formatResult: (method: string, result: Record<string, unknown> | undefined, _pending: PendingRequest | undefined, ctx: FormatterContext) => void;
|
package/dist/result-formatter.js
DELETED
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
import { truncate } from "./utils.js";
|
|
2
|
-
/**
|
|
3
|
-
* Format the result of a request and log it.
|
|
4
|
-
*
|
|
5
|
-
* @param method The method of the request
|
|
6
|
-
* @param result The result of the request
|
|
7
|
-
* @param _pending The pending request associated with this result
|
|
8
|
-
* @param ctx The formatter context containing the logger and colors
|
|
9
|
-
* @returns void
|
|
10
|
-
*/
|
|
11
|
-
export const formatResult = (method, result, _pending, ctx) => {
|
|
12
|
-
if (!result)
|
|
13
|
-
return;
|
|
14
|
-
const { logger, colors: c, plainColors: pc } = ctx;
|
|
15
|
-
if (method === "tools/list" && Array.isArray(result.tools)) {
|
|
16
|
-
const tools = result.tools;
|
|
17
|
-
const names = tools.map((t) => t.name);
|
|
18
|
-
const summary = names.length <= 8
|
|
19
|
-
? names.join(", ")
|
|
20
|
-
: names.slice(0, 6).join(", ") + `, ... +${names.length - 6} more`;
|
|
21
|
-
logger.write(` ${c.GREEN}(${tools.length} tools: ${summary})${c.RESET}`, ` (${tools.length} tools: ${summary})`);
|
|
22
|
-
return;
|
|
23
|
-
}
|
|
24
|
-
if (method === "resources/list" && Array.isArray(result.resources)) {
|
|
25
|
-
const resources = result.resources;
|
|
26
|
-
const names = resources.map((r) => r.name || r.uri);
|
|
27
|
-
const summary = names.length <= 8
|
|
28
|
-
? names.join(", ")
|
|
29
|
-
: names.slice(0, 6).join(", ") + `, ... +${names.length - 6} more`;
|
|
30
|
-
logger.write(` ${c.CYAN}(${resources.length} resources: ${summary})${c.RESET}`, ` (${resources.length} resources: ${summary})`);
|
|
31
|
-
return;
|
|
32
|
-
}
|
|
33
|
-
// prompts/list — summarize
|
|
34
|
-
if (method === "prompts/list" && Array.isArray(result.prompts)) {
|
|
35
|
-
const prompts = result.prompts;
|
|
36
|
-
const names = prompts.map((p) => p.name);
|
|
37
|
-
const summary = names.length <= 8
|
|
38
|
-
? names.join(", ")
|
|
39
|
-
: names.slice(0, 6).join(", ") + `, ... +${names.length - 6} more`;
|
|
40
|
-
logger.write(` ${c.YELLOW}(${prompts.length} prompts: ${summary})${c.RESET}`, ` (${prompts.length} prompts: ${summary})`);
|
|
41
|
-
return;
|
|
42
|
-
}
|
|
43
|
-
// tools/call — show content blocks
|
|
44
|
-
if (method === "tools/call" && Array.isArray(result.content)) {
|
|
45
|
-
const content = result.content;
|
|
46
|
-
for (const block of content) {
|
|
47
|
-
if (block.type === "text") {
|
|
48
|
-
const lines = block.text.split("\n");
|
|
49
|
-
for (const line of lines) {
|
|
50
|
-
logger.write(` ${c.DIM}[text]${c.RESET} ${line}`, ` [text] ${line}`);
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
else if (block.type === "image") {
|
|
54
|
-
const data = block.data;
|
|
55
|
-
const size = data
|
|
56
|
-
? `${Math.round((data.length * 3) / 4 / 1024)}KB`
|
|
57
|
-
: "?";
|
|
58
|
-
logger.write(` ${c.DIM}[image ${block.mimeType || "?"}]${c.RESET} ${size}`, ` [image ${block.mimeType || "?"}] ${size}`);
|
|
59
|
-
}
|
|
60
|
-
else if (block.type === "resource") {
|
|
61
|
-
const resource = block.resource;
|
|
62
|
-
const uri = resource?.uri || "?";
|
|
63
|
-
logger.write(` ${c.DIM}[resource]${c.RESET} ${truncate(uri)}`, ` [resource] ${truncate(uri)}`);
|
|
64
|
-
}
|
|
65
|
-
else {
|
|
66
|
-
const raw = truncate(JSON.stringify(block), 200);
|
|
67
|
-
logger.write(` ${c.DIM}[${block.type || "?"}]${c.RESET} ${raw}`, ` [${block.type || "?"}] ${raw}`);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
if (result.isError) {
|
|
71
|
-
logger.write(` ${c.RED}(isError: true)${c.RESET}`, ` (isError: true)`);
|
|
72
|
-
}
|
|
73
|
-
return;
|
|
74
|
-
}
|
|
75
|
-
// initialize — show capabilities
|
|
76
|
-
if (method === "initialize" && result.capabilities) {
|
|
77
|
-
const caps = Object.keys(result.capabilities).join(", ");
|
|
78
|
-
const serverInfo = result.serverInfo;
|
|
79
|
-
const name = serverInfo?.name || "?";
|
|
80
|
-
const version = serverInfo?.version || "?";
|
|
81
|
-
logger.write(` ${c.MAGENTA}${name} v${version}${c.RESET} capabilities: ${caps}`, ` ${name} v${version} capabilities: ${caps}`);
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
84
|
-
// Generic: show truncated JSON
|
|
85
|
-
const raw = JSON.stringify(result);
|
|
86
|
-
if (raw.length > 2) {
|
|
87
|
-
const display = truncate(raw, 300);
|
|
88
|
-
logger.write(` ${c.DIM}${display}${c.RESET}`, ` ${display}`);
|
|
89
|
-
}
|
|
90
|
-
};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi } from "vitest";
|
|
2
|
-
import { formatResult } from "./result-formatter.js";
|
|
3
|
-
import { PLAIN } from "./colors.js";
|
|
4
|
-
function createMockCtx() {
|
|
5
|
-
const written = [];
|
|
6
|
-
const logger = {
|
|
7
|
-
write: vi.fn((colored, plain) => {
|
|
8
|
-
written.push({ colored, plain });
|
|
9
|
-
}),
|
|
10
|
-
};
|
|
11
|
-
return {
|
|
12
|
-
ctx: { logger, colors: PLAIN, plainColors: PLAIN },
|
|
13
|
-
written,
|
|
14
|
-
logger,
|
|
15
|
-
};
|
|
16
|
-
}
|
|
17
|
-
describe("formatResult", () => {
|
|
18
|
-
it("does nothing for undefined result", () => {
|
|
19
|
-
const { logger } = createMockCtx();
|
|
20
|
-
formatResult("tools/list", undefined, undefined, {
|
|
21
|
-
logger,
|
|
22
|
-
colors: PLAIN,
|
|
23
|
-
plainColors: PLAIN,
|
|
24
|
-
});
|
|
25
|
-
expect(logger.write).not.toHaveBeenCalled();
|
|
26
|
-
});
|
|
27
|
-
it("summarizes tools/list", () => {
|
|
28
|
-
const { ctx, written } = createMockCtx();
|
|
29
|
-
formatResult("tools/list", {
|
|
30
|
-
tools: [{ name: "echo" }, { name: "add" }],
|
|
31
|
-
}, undefined, ctx);
|
|
32
|
-
expect(written.length).toBe(1);
|
|
33
|
-
expect(written[0].plain).toContain("2 tools");
|
|
34
|
-
expect(written[0].plain).toContain("echo, add");
|
|
35
|
-
});
|
|
36
|
-
it("truncates long tools/list", () => {
|
|
37
|
-
const { ctx, written } = createMockCtx();
|
|
38
|
-
const tools = Array.from({ length: 20 }, (_, i) => ({ name: `tool${i}` }));
|
|
39
|
-
formatResult("tools/list", { tools }, undefined, ctx);
|
|
40
|
-
expect(written[0].plain).toContain("20 tools");
|
|
41
|
-
expect(written[0].plain).toContain("+14 more");
|
|
42
|
-
});
|
|
43
|
-
it("summarizes resources/list", () => {
|
|
44
|
-
const { ctx, written } = createMockCtx();
|
|
45
|
-
formatResult("resources/list", {
|
|
46
|
-
resources: [{ name: "readme", uri: "file:///readme" }],
|
|
47
|
-
}, undefined, ctx);
|
|
48
|
-
expect(written[0].plain).toContain("1 resources");
|
|
49
|
-
expect(written[0].plain).toContain("readme");
|
|
50
|
-
});
|
|
51
|
-
it("summarizes prompts/list", () => {
|
|
52
|
-
const { ctx, written } = createMockCtx();
|
|
53
|
-
formatResult("prompts/list", {
|
|
54
|
-
prompts: [{ name: "greeting" }],
|
|
55
|
-
}, undefined, ctx);
|
|
56
|
-
expect(written[0].plain).toContain("1 prompts");
|
|
57
|
-
expect(written[0].plain).toContain("greeting");
|
|
58
|
-
});
|
|
59
|
-
it("formats tools/call text content", () => {
|
|
60
|
-
const { ctx, written } = createMockCtx();
|
|
61
|
-
formatResult("tools/call", {
|
|
62
|
-
content: [{ type: "text", text: "hello\nworld" }],
|
|
63
|
-
}, undefined, ctx);
|
|
64
|
-
expect(written.length).toBe(2);
|
|
65
|
-
expect(written[0].plain).toContain("[text] hello");
|
|
66
|
-
expect(written[1].plain).toContain("[text] world");
|
|
67
|
-
});
|
|
68
|
-
it("formats tools/call image content", () => {
|
|
69
|
-
const { ctx, written } = createMockCtx();
|
|
70
|
-
formatResult("tools/call", {
|
|
71
|
-
content: [{ type: "image", mimeType: "image/png", data: "a".repeat(4096) }],
|
|
72
|
-
}, undefined, ctx);
|
|
73
|
-
expect(written[0].plain).toContain("[image image/png]");
|
|
74
|
-
expect(written[0].plain).toContain("KB");
|
|
75
|
-
});
|
|
76
|
-
it("shows isError flag", () => {
|
|
77
|
-
const { ctx, written } = createMockCtx();
|
|
78
|
-
formatResult("tools/call", {
|
|
79
|
-
content: [{ type: "text", text: "fail" }],
|
|
80
|
-
isError: true,
|
|
81
|
-
}, undefined, ctx);
|
|
82
|
-
const last = written[written.length - 1];
|
|
83
|
-
expect(last.plain).toContain("isError: true");
|
|
84
|
-
});
|
|
85
|
-
it("formats initialize capabilities", () => {
|
|
86
|
-
const { ctx, written } = createMockCtx();
|
|
87
|
-
formatResult("initialize", {
|
|
88
|
-
capabilities: { tools: {}, resources: {} },
|
|
89
|
-
serverInfo: { name: "test-server", version: "1.0" },
|
|
90
|
-
}, undefined, ctx);
|
|
91
|
-
expect(written[0].plain).toContain("test-server v1.0");
|
|
92
|
-
expect(written[0].plain).toContain("tools, resources");
|
|
93
|
-
});
|
|
94
|
-
it("shows truncated JSON for unknown methods", () => {
|
|
95
|
-
const { ctx, written } = createMockCtx();
|
|
96
|
-
formatResult("unknown/method", { foo: "bar" }, undefined, ctx);
|
|
97
|
-
expect(written[0].plain).toContain("foo");
|
|
98
|
-
});
|
|
99
|
-
});
|
package/dist/utils.d.ts
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Get the current timestamp in the format HH:MM:SS.mmm
|
|
3
|
-
* @returns The current timestamp in the format HH:MM:SS.mmm
|
|
4
|
-
*/
|
|
5
|
-
export declare const timestamp: () => string;
|
|
6
|
-
/**
|
|
7
|
-
* Truncate a string to a maximum length, adding "..." if truncated.
|
|
8
|
-
* @param str The string to truncate
|
|
9
|
-
* @param max The maximum length of the string
|
|
10
|
-
* @returns The truncated string
|
|
11
|
-
*/
|
|
12
|
-
export declare const truncate: (str: string, max?: number) => string;
|
package/dist/utils.js
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Get the current timestamp in the format HH:MM:SS.mmm
|
|
3
|
-
* @returns The current timestamp in the format HH:MM:SS.mmm
|
|
4
|
-
*/
|
|
5
|
-
export const timestamp = () => {
|
|
6
|
-
const d = new Date();
|
|
7
|
-
const h = String(d.getHours()).padStart(2, "0");
|
|
8
|
-
const m = String(d.getMinutes()).padStart(2, "0");
|
|
9
|
-
const s = String(d.getSeconds()).padStart(2, "0");
|
|
10
|
-
const ms = String(d.getMilliseconds()).padStart(3, "0");
|
|
11
|
-
return `${h}:${m}:${s}.${ms}`;
|
|
12
|
-
};
|
|
13
|
-
/**
|
|
14
|
-
* Truncate a string to a maximum length, adding "..." if truncated.
|
|
15
|
-
* @param str The string to truncate
|
|
16
|
-
* @param max The maximum length of the string
|
|
17
|
-
* @returns The truncated string
|
|
18
|
-
*/
|
|
19
|
-
export const truncate = (str, max = 200) => {
|
|
20
|
-
if (str.length <= max)
|
|
21
|
-
return str;
|
|
22
|
-
return str.slice(0, max - 3) + "...";
|
|
23
|
-
};
|
package/dist/utils.test.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
package/dist/utils.test.js
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { timestamp, truncate } from "./utils.js";
|
|
3
|
-
describe("timestamp", () => {
|
|
4
|
-
it("returns HH:MM:SS.mmm format", () => {
|
|
5
|
-
const ts = timestamp();
|
|
6
|
-
expect(ts).toMatch(/^\d{2}:\d{2}:\d{2}\.\d{3}$/);
|
|
7
|
-
});
|
|
8
|
-
});
|
|
9
|
-
describe("truncate", () => {
|
|
10
|
-
it("returns short strings unchanged", () => {
|
|
11
|
-
expect(truncate("hello", 200)).toBe("hello");
|
|
12
|
-
});
|
|
13
|
-
it("truncates long strings with ellipsis", () => {
|
|
14
|
-
const long = "a".repeat(300);
|
|
15
|
-
const result = truncate(long, 200);
|
|
16
|
-
expect(result.length).toBe(200);
|
|
17
|
-
expect(result.endsWith("...")).toBe(true);
|
|
18
|
-
});
|
|
19
|
-
it("returns string at exact max length unchanged", () => {
|
|
20
|
-
const exact = "b".repeat(200);
|
|
21
|
-
expect(truncate(exact, 200)).toBe(exact);
|
|
22
|
-
});
|
|
23
|
-
it("uses default max of 200", () => {
|
|
24
|
-
const long = "c".repeat(250);
|
|
25
|
-
const result = truncate(long);
|
|
26
|
-
expect(result.length).toBe(200);
|
|
27
|
-
});
|
|
28
|
-
});
|
|
File without changes
|