ilya 1.0.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 AugmentedMagic
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,166 @@
1
+ # ilya
2
+
3
+ See exactly what your MCP client sends and your server responds.
4
+
5
+ A transparent stdio proxy that sits between any MCP client (Cursor, Claude Desktop, Claude Code) and any MCP server, logging every JSON-RPC message so you can actually see what's happening.
6
+
7
+ ```
8
+ Cursor / Claude Desktop / Claude Code
9
+
10
+ │ ←────────── spawns (thinks this IS the MCP server)
11
+
12
+ ┌───────────┐
13
+ │ ilya │ ←─ logs every message
14
+ └───────────┘
15
+
16
+ │ ←────────── spawns the real server
17
+
18
+ ┌────────────────────┐
19
+ │ actual MCP server │
20
+ └────────────────────┘
21
+ ```
22
+
23
+ ## Why
24
+
25
+ When Cursor or Claude Desktop spawns an MCP server, it owns the child process. All stdio communication is invisible. There's no terminal, no logs, no way to see what the AI is asking or what the server responds with.
26
+
27
+ `ilya` fixes that. Zero dependencies, zero config. Just prefix your command.
28
+
29
+ <!-- TODO: terminal recording gif -->
30
+
31
+ ## Install
32
+
33
+ ```bash
34
+ npm install -g ilya
35
+ ```
36
+
37
+ Or use directly with npx:
38
+
39
+ ```bash
40
+ npx ilya node ./my-server.js
41
+ ```
42
+
43
+ ## Usage
44
+
45
+ Prefix your MCP server command with `ilya`:
46
+
47
+ ```bash
48
+ ilya node ./my-server.js
49
+ ilya python ./server.py
50
+ ilya npx ts-node ./server.ts
51
+ ilya ./my-binary --flag
52
+ ```
53
+
54
+ Logs are written to `~/.ilya/logs/<server>-<pid>.log` by default. The log path is printed to stderr on startup.
55
+
56
+ ### Flags
57
+
58
+ ```
59
+ --log, -l <path> Write logs to a specific file
60
+ --port, -p <port> Start an HTTP server to stream logs in real time
61
+ --help, -h Show help
62
+ --version, -v Show version
63
+ ```
64
+
65
+ ### Integration with MCP clients
66
+
67
+ In your MCP client config, just wrap the command with `npx ilya`:
68
+
69
+ **Cursor / Claude Desktop (`mcp.json` or settings):**
70
+
71
+ ```json
72
+ {
73
+ "mcpServers": {
74
+ "my-server": {
75
+ "command": "npx",
76
+ "args": ["ilya", "node", "./my-server.js"]
77
+ }
78
+ }
79
+ }
80
+ ```
81
+
82
+ **Claude Code (`.mcp.json`):**
83
+
84
+ ```json
85
+ {
86
+ "mcpServers": {
87
+ "my-server": {
88
+ "command": "npx",
89
+ "args": ["ilya", "node", "./my-server.js"]
90
+ }
91
+ }
92
+ }
93
+ ```
94
+
95
+ Then tail the log file in a separate terminal:
96
+
97
+ ```bash
98
+ tail -f ~/.ilya/logs/node-*.log
99
+ ```
100
+
101
+ Or use `--port` to stream logs over HTTP:
102
+
103
+ ```json
104
+ {
105
+ "mcpServers": {
106
+ "my-server": {
107
+ "command": "npx",
108
+ "args": ["ilya", "--port", "3456", "node", "./my-server.js"]
109
+ }
110
+ }
111
+ }
112
+ ```
113
+
114
+ ```bash
115
+ curl http://localhost:3456
116
+ ```
117
+
118
+ ## Example output
119
+
120
+ ```
121
+ 12:34:56.100 → CLIENT notification notifications/initialized
122
+ 12:34:56.200 → CLIENT request initialize #1
123
+ {"protocolVersion":"2024-11-05","capabilities":{}}
124
+ 12:34:56.250 ← SERVER response initialize #1 50ms
125
+ my-server v1.0.0 capabilities: tools, resources
126
+ 12:34:56.300 → CLIENT request tools/list #2
127
+ 12:34:56.310 ← SERVER response tools/list #2 10ms
128
+ (5 tools: get_overview, analyze_error, search_logs, get_config, restart)
129
+ 12:34:56.500 → CLIENT request tools/call get_overview #3
130
+ {
131
+ "query": "why is my app slow"
132
+ }
133
+ 12:34:57.100 ← SERVER response tools/call #3 600ms
134
+ [text] Found 3 sessions with performance issues...
135
+ ```
136
+
137
+ Errors are clearly highlighted:
138
+
139
+ ```
140
+ 12:34:57.456 ← SERVER ERROR (tools/call #3) 12ms
141
+ [-32601] Method not found
142
+ ```
143
+
144
+ Server stderr is forwarded with a prefix:
145
+
146
+ ```
147
+ [server stderr] Listening on stdio
148
+ [server stderr] Connected to database
149
+ ```
150
+
151
+ ## How it works
152
+
153
+ 1. Your MCP client spawns `ilya` instead of the real server
154
+ 2. `ilya` spawns the real server as a child process
155
+ 3. Every stdin line from the client is logged and forwarded to the server
156
+ 4. Every stdout line from the server is logged and forwarded to the client
157
+ 5. Messages are never modified, delayed, or buffered — fully transparent
158
+ 6. Logs go to a file (and optionally stderr/HTTP) so they never interfere with the stdio protocol
159
+
160
+ ## Works with everything
161
+
162
+ Any MCP server in any language. If it speaks JSON-RPC over stdio, `ilya` can log it.
163
+
164
+ ## License
165
+
166
+ MIT
package/bin/mcp-tap.js ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import("../dist/cli.js");
package/dist/cli.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Run the CLI application.
3
+ */
4
+ export declare const run: () => void;
package/dist/cli.js ADDED
@@ -0,0 +1,85 @@
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();
@@ -0,0 +1,28 @@
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 ADDED
@@ -0,0 +1,68 @@
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
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,48 @@
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
+ });
@@ -0,0 +1,18 @@
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;
@@ -0,0 +1,95 @@
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
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,100 @@
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
+ });
@@ -0,0 +1,9 @@
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;
@@ -0,0 +1,26 @@
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
+ };
@@ -0,0 +1,7 @@
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);
@@ -0,0 +1,20 @@
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
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,44 @@
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
+ });
@@ -0,0 +1,16 @@
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 ADDED
@@ -0,0 +1,51 @@
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
+ }
@@ -0,0 +1,16 @@
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 ADDED
@@ -0,0 +1,100 @@
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
+ };
@@ -0,0 +1,22 @@
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;
@@ -0,0 +1,90 @@
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
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,99 @@
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
+ });
@@ -0,0 +1,12 @@
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 ADDED
@@ -0,0 +1,23 @@
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
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,28 @@
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
+ });
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "ilya",
3
+ "version": "1.0.0",
4
+ "description": "See exactly what your MCP client sends and your server responds. Zero config, zero dependencies.",
5
+ "type": "module",
6
+ "bin": {
7
+ "ilya": "./bin/ilya.js"
8
+ },
9
+ "files": [
10
+ "bin",
11
+ "dist"
12
+ ],
13
+ "scripts": {
14
+ "build": "tsc",
15
+ "test": "npx vitest run",
16
+ "test:coverage": "npx vitest run --coverage",
17
+ "test:watch": "npx vitest"
18
+ },
19
+ "devDependencies": {
20
+ "@types/node": "^22",
21
+ "@vitest/coverage-v8": "4.0.18",
22
+ "typescript": "^5"
23
+ },
24
+ "keywords": [
25
+ "mcp",
26
+ "model-context-protocol",
27
+ "debug",
28
+ "inspector",
29
+ "devtools",
30
+ "stdio",
31
+ "proxy",
32
+ "logging"
33
+ ],
34
+ "license": "MIT",
35
+ "repository": {
36
+ "type": "git",
37
+ "url": "https://github.com/AugmentedMagic/ilya"
38
+ },
39
+ "engines": {
40
+ "node": ">=18"
41
+ }
42
+ }