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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ilya",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "See exactly what your MCP client sends and your server responds. Zero config, zero dependencies.",
5
5
  "type": "module",
6
6
  "bin": {
package/dist/cli.d.ts DELETED
@@ -1,4 +0,0 @@
1
- /**
2
- * Run the CLI application.
3
- */
4
- export declare const run: () => void;
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
- };
@@ -1 +0,0 @@
1
- export {};
@@ -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
- });
@@ -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
- };
@@ -1 +0,0 @@
1
- export {};
@@ -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
- });
@@ -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;
@@ -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
- };
@@ -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);
@@ -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 {};
@@ -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;
@@ -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
- };
@@ -1 +0,0 @@
1
- export {};
@@ -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