@hasna/logs 0.3.21 → 0.3.22

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.
@@ -0,0 +1,15 @@
1
+ // @bun
2
+ import {
3
+ getLogContext,
4
+ getLogContextFromId,
5
+ searchLogs,
6
+ tailLogs
7
+ } from "./index-ww5ggfv3.js";
8
+ import"./index-997bkzr2.js";
9
+ import"./index-re3ntm60.js";
10
+ export {
11
+ tailLogs,
12
+ searchLogs,
13
+ getLogContextFromId,
14
+ getLogContext
15
+ };
@@ -6,7 +6,11 @@ import {
6
6
  setPageAuth,
7
7
  setRetentionPolicy,
8
8
  startScheduler
9
- } from "../index-4ba0sabv.js";
9
+ } from "../index-ssqkc6nh.js";
10
+ import {
11
+ exportToCsv,
12
+ exportToJson
13
+ } from "../index-eh9bkbpa.js";
10
14
  import {
11
15
  getHealth
12
16
  } from "../index-cpvq9np9.js";
@@ -15,6 +19,7 @@ import {
15
19
  createPage,
16
20
  createProject,
17
21
  deleteAlertRule,
22
+ exitIfMetadataRequest,
18
23
  getDb,
19
24
  getIssue,
20
25
  getLatestSnapshot,
@@ -26,12 +31,13 @@ import {
26
31
  listIssues,
27
32
  listPages,
28
33
  listProjects,
34
+ readOptionValue,
29
35
  resolveProjectId,
30
36
  summarizeLogs,
31
37
  updateAlertRule,
32
38
  updateIssueStatus,
33
39
  updateProject
34
- } from "../index-vmr85wa1.js";
40
+ } from "../index-5cj74qka.js";
35
41
  import {
36
42
  createJob,
37
43
  deleteJob,
@@ -49,10 +55,6 @@ import {
49
55
  import {
50
56
  parseTime
51
57
  } from "../index-997bkzr2.js";
52
- import {
53
- exportToCsv,
54
- exportToJson
55
- } from "../index-eh9bkbpa.js";
56
58
  import"../index-re3ntm60.js";
57
59
 
58
60
  // node_modules/hono/dist/compose.js
@@ -2442,7 +2444,13 @@ function streamRoutes(db) {
2442
2444
  }
2443
2445
 
2444
2446
  // src/server/index.ts
2445
- var PORT = Number(process.env.LOGS_PORT ?? 3460);
2447
+ exitIfMetadataRequest({
2448
+ name: "logs-serve",
2449
+ description: "Start the @hasna/logs REST API server.",
2450
+ options: [" -p, --port <n> Port to listen on (default: LOGS_PORT or 3460)"]
2451
+ });
2452
+ var portArg = readOptionValue(["--port", "-p"]);
2453
+ var PORT = Number(portArg ?? process.env.LOGS_PORT ?? 3460);
2446
2454
  var db = getDb();
2447
2455
  var app = new Hono2;
2448
2456
  app.use("*", cors());
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/logs",
3
- "version": "0.3.21",
3
+ "version": "0.3.22",
4
4
  "description": "Log aggregation + browser script + headless page scanner + performance monitoring for AI agents",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -11,7 +11,7 @@
11
11
  "logs-serve": "./dist/server/index.js"
12
12
  },
13
13
  "scripts": {
14
- "build": "bun build src/cli/index.ts src/mcp/index.ts src/server/index.ts --outdir dist --target bun --splitting --external playwright --external playwright-core --external electron --external chromium-bidi --external lighthouse",
14
+ "build": "bun build src/cli/index.ts src/mcp/index.ts src/server/index.ts src/index.ts --outdir dist --target bun --splitting --external playwright --external playwright-core --external electron --external chromium-bidi --external lighthouse",
15
15
  "build:dashboard": "cd dashboard && bun run build",
16
16
  "build:all": "bun run build:dashboard && bun run build",
17
17
  "dev": "bun run src/server/index.ts",
@@ -41,7 +41,7 @@
41
41
  "license": "Apache-2.0",
42
42
  "dependencies": {
43
43
  "@hasna/cloud": "^0.1.24",
44
- "@modelcontextprotocol/sdk": "^1.12.1",
44
+ "@modelcontextprotocol/sdk": "^1.29.0",
45
45
  "commander": "^14.0.0",
46
46
  "hono": "^4.7.11",
47
47
  "ink": "^5.1.0",
package/sdk/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@hasna/logs-sdk",
3
3
  "version": "0.1.0",
4
- "description": "Zero-dependency fetch client for @hasna/logs push logs, query, browse issues, perf snapshots",
4
+ "description": "Zero-dependency fetch client for @hasna/logs \u2014 push logs, query, browse issues, perf snapshots",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "types": "./dist/index.d.ts",
@@ -16,7 +16,12 @@
16
16
  "scripts": {
17
17
  "build": "bun build src/index.ts --outdir dist --target browser"
18
18
  },
19
- "keywords": ["logs", "monitoring", "sdk", "ai-agents"],
19
+ "keywords": [
20
+ "logs",
21
+ "monitoring",
22
+ "sdk",
23
+ "ai-agents"
24
+ ],
20
25
  "author": "Andrei Hasna <andrei@hasna.com>",
21
- "license": "MIT"
26
+ "license": "Apache-2.0"
22
27
  }
@@ -46,7 +46,7 @@ test("logs-mcp --help prints usage and exits without starting stdio transport",
46
46
 
47
47
  expect(result.exitCode).toBe(0)
48
48
  expect(result.stdout).toContain("Usage: logs-mcp [options]")
49
- expect(result.stdout).toContain("Start the @hasna/logs MCP server over stdio.")
49
+ expect(result.stdout).toContain("Start the @hasna/logs MCP server (stdio by default).")
50
50
  expect(result.stdout).not.toContain("Listening")
51
51
  expect(result.stderr.trim()).toBe("")
52
52
  })
package/src/index.ts ADDED
@@ -0,0 +1 @@
1
+ export * from "../sdk/src/index.ts";
@@ -0,0 +1,92 @@
1
+ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
2
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
3
+ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
4
+ import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
5
+ import { buildServer } from "./index.ts";
6
+ import {
7
+ DEFAULT_MCP_HTTP_PORT,
8
+ isHttpMode,
9
+ resolveMcpHttpPort,
10
+ startMcpHttpServer,
11
+ } from "./http.ts";
12
+
13
+ describe("logs MCP HTTP transport", () => {
14
+ test("defaults port to 8820", () => {
15
+ expect(DEFAULT_MCP_HTTP_PORT).toBe(8820);
16
+ expect(resolveMcpHttpPort(["node"], {})).toBe(8820);
17
+ expect(resolveMcpHttpPort(["node", "--port", "9001"], {})).toBe(9001);
18
+ expect(resolveMcpHttpPort(["node"], { MCP_HTTP_PORT: "9002" })).toBe(9002);
19
+ });
20
+
21
+ test("isHttpMode detects flag and env", () => {
22
+ expect(isHttpMode(["node"], {})).toBe(false);
23
+ expect(isHttpMode(["node", "--http"], {})).toBe(true);
24
+ expect(isHttpMode(["node"], { MCP_HTTP: "1" })).toBe(true);
25
+ });
26
+ });
27
+
28
+ describe("logs buildServer stdio registration", () => {
29
+ test("registers tools over in-memory transport", async () => {
30
+ const server = buildServer();
31
+ const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
32
+ await server.connect(serverTransport);
33
+
34
+ const client = new Client({ name: "test", version: "0.0.0" });
35
+ await client.connect(clientTransport);
36
+
37
+ const tools = await client.listTools();
38
+ expect(tools.tools.some((tool) => tool.name === "get_health")).toBe(true);
39
+
40
+ await client.close();
41
+ await server.close();
42
+ });
43
+ });
44
+
45
+ describe("logs streamable HTTP server", () => {
46
+ let handle: Awaited<ReturnType<typeof startMcpHttpServer>>;
47
+
48
+ beforeAll(async () => {
49
+ handle = await startMcpHttpServer(buildServer, { port: 0 });
50
+ });
51
+
52
+ afterAll(async () => {
53
+ await handle.close();
54
+ });
55
+
56
+ test("GET /health returns ok", async () => {
57
+ const res = await fetch(`http://${handle.host}:${handle.port}/health`);
58
+ expect(res.status).toBe(200);
59
+ expect(await res.json()).toEqual({ status: "ok", name: "logs" });
60
+ });
61
+
62
+ test("initialize and call get_health over streamable HTTP", async () => {
63
+ const transport = new StreamableHTTPClientTransport(
64
+ new URL(`http://${handle.host}:${handle.port}/mcp`),
65
+ );
66
+ const client = new Client({ name: "test", version: "0.0.0" });
67
+ await client.connect(transport);
68
+
69
+ const result = await client.callTool({ name: "get_health", arguments: {} });
70
+ expect(result.content).toBeDefined();
71
+ expect(Array.isArray(result.content)).toBe(true);
72
+
73
+ await client.close();
74
+ });
75
+
76
+ test("serves three concurrent clients from one process", async () => {
77
+ const clients = await Promise.all(
78
+ Array.from({ length: 3 }, async () => {
79
+ const transport = new StreamableHTTPClientTransport(
80
+ new URL(`http://${handle.host}:${handle.port}/mcp`),
81
+ );
82
+ const client = new Client({ name: "test", version: "0.0.0" });
83
+ await client.connect(transport);
84
+ const tools = await client.listTools();
85
+ return { client, count: tools.tools.length };
86
+ }),
87
+ );
88
+
89
+ expect(clients.every((entry) => entry.count > 0)).toBe(true);
90
+ await Promise.all(clients.map((entry) => entry.client.close()));
91
+ });
92
+ });
@@ -0,0 +1,128 @@
1
+ import { createServer, type IncomingMessage, type ServerResponse } from "node:http";
2
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
3
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
4
+
5
+ export const MCP_HTTP_SERVICE_NAME = "logs";
6
+ export const DEFAULT_MCP_HTTP_PORT = 8820;
7
+
8
+ export function isHttpMode(
9
+ argv: string[] = process.argv,
10
+ env: NodeJS.ProcessEnv = process.env,
11
+ ): boolean {
12
+ return argv.includes("--http") || env.MCP_HTTP === "1";
13
+ }
14
+
15
+ export function resolveMcpHttpPort(
16
+ argv: string[] = process.argv,
17
+ env: NodeJS.ProcessEnv = process.env,
18
+ ): number {
19
+ const portIdx = argv.indexOf("--port");
20
+ if (portIdx !== -1 && argv[portIdx + 1]) {
21
+ return parsePort(argv[portIdx + 1]!, "--port");
22
+ }
23
+ if (env.MCP_HTTP_PORT) {
24
+ return parsePort(env.MCP_HTTP_PORT, "MCP_HTTP_PORT");
25
+ }
26
+ return DEFAULT_MCP_HTTP_PORT;
27
+ }
28
+
29
+ function parsePort(raw: string, source: string): number {
30
+ const parsed = Number(raw);
31
+ if (!Number.isInteger(parsed) || parsed < 0 || parsed > 65535) {
32
+ throw new Error(`Invalid ${source} value "${raw}". Expected 0-65535.`);
33
+ }
34
+ return parsed;
35
+ }
36
+
37
+ async function readJsonBody(req: IncomingMessage): Promise<unknown> {
38
+ const chunks: Buffer[] = [];
39
+ for await (const chunk of req) {
40
+ chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
41
+ }
42
+ const text = Buffer.concat(chunks).toString("utf8");
43
+ if (!text) return undefined;
44
+ return JSON.parse(text) as unknown;
45
+ }
46
+
47
+ export type McpHttpServerHandle = {
48
+ port: number;
49
+ host: string;
50
+ close: () => Promise<void>;
51
+ };
52
+
53
+ export async function startMcpHttpServer(
54
+ buildServer: () => McpServer,
55
+ options?: { port?: number; host?: string; serviceName?: string },
56
+ ): Promise<McpHttpServerHandle> {
57
+ const host = options?.host ?? "127.0.0.1";
58
+ const requestedPort = options?.port ?? resolveMcpHttpPort();
59
+ const serviceName = options?.serviceName ?? MCP_HTTP_SERVICE_NAME;
60
+
61
+ const httpServer = createServer(async (req: IncomingMessage, res: ServerResponse) => {
62
+ try {
63
+ const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
64
+
65
+ if (req.method === "GET" && url.pathname === "/health") {
66
+ res.writeHead(200, { "Content-Type": "application/json" });
67
+ res.end(JSON.stringify({ status: "ok", name: serviceName }));
68
+ return;
69
+ }
70
+
71
+ if (url.pathname !== "/mcp") {
72
+ res.writeHead(404, { "Content-Type": "text/plain" });
73
+ res.end("Not Found");
74
+ return;
75
+ }
76
+
77
+ const server = buildServer();
78
+ const transport = new StreamableHTTPServerTransport({
79
+ sessionIdGenerator: undefined,
80
+ });
81
+
82
+ await server.connect(transport);
83
+
84
+ let parsedBody: unknown;
85
+ if (req.method === "POST") {
86
+ parsedBody = await readJsonBody(req);
87
+ }
88
+
89
+ await transport.handleRequest(req, res, parsedBody);
90
+
91
+ res.on("close", () => {
92
+ void transport.close();
93
+ void server.close();
94
+ });
95
+ } catch (error) {
96
+ console.error(`[${serviceName}-mcp] HTTP error:`, error);
97
+ if (!res.headersSent) {
98
+ res.writeHead(500, { "Content-Type": "application/json" });
99
+ res.end(
100
+ JSON.stringify({
101
+ jsonrpc: "2.0",
102
+ error: { code: -32603, message: "Internal server error" },
103
+ id: null,
104
+ }),
105
+ );
106
+ }
107
+ }
108
+ });
109
+
110
+ await new Promise<void>((resolve, reject) => {
111
+ httpServer.once("error", reject);
112
+ httpServer.listen(requestedPort, host, () => resolve());
113
+ });
114
+
115
+ const addr = httpServer.address();
116
+ const port = typeof addr === "object" && addr ? addr.port : requestedPort;
117
+
118
+ console.error(`[${serviceName}-mcp] Streamable HTTP listening on http://${host}:${port}/mcp`);
119
+
120
+ return {
121
+ port,
122
+ host,
123
+ close: () =>
124
+ new Promise<void>((resolve, reject) => {
125
+ httpServer.close((err) => (err ? reject(err) : resolve()));
126
+ }),
127
+ };
128
+ }
package/src/mcp/index.ts CHANGED
@@ -24,16 +24,22 @@ import type { LogLevel, LogRow } from "../types/index.ts"
24
24
 
25
25
  exitIfMetadataRequest({
26
26
  name: "logs-mcp",
27
- description: "Start the @hasna/logs MCP server over stdio.",
27
+ description: "Start the @hasna/logs MCP server (stdio by default).",
28
+ options: [
29
+ " --http Serve MCP over Streamable HTTP (127.0.0.1)",
30
+ " --port <number> HTTP port (default: 8820, env: MCP_HTTP_PORT)",
31
+ ],
28
32
  })
29
33
 
30
34
  const db = getDb()
31
- const server = new McpServer({ name: "logs", version: PACKAGE_VERSION })
32
35
 
33
- // --- in-memory agent registry ---
36
+ // --- in-memory agent registry (module-level for shared HTTP process) ---
34
37
  interface _LogsAgent { id: string; name: string; session_id?: string; last_seen_at: string; project_id?: string }
35
38
  const _logsAgents = new Map<string, _LogsAgent>()
36
39
 
40
+ export function buildServer(): McpServer {
41
+ const server = new McpServer({ name: "logs", version: PACKAGE_VERSION })
42
+
37
43
  const BRIEF_FIELDS: (keyof LogRow)[] = ["id", "timestamp", "level", "message", "service"]
38
44
 
39
45
  function applyBrief(rows: LogRow[], brief = true): unknown[] {
@@ -408,6 +414,30 @@ server.tool("list_agents", "List all registered agents.", {}, async () => {
408
414
  return { content: [{ type: "text" as const, text: JSON.stringify([..._logsAgents.values()]) }] }
409
415
  })
410
416
 
411
- const transport = new StdioServerTransport()
412
- registerCloudTools(server, "logs")
413
- await server.connect(transport)
417
+ registerCloudTools(server, "logs")
418
+ return server
419
+ }
420
+
421
+ async function main(): Promise<void> {
422
+ const { isHttpMode, resolveMcpHttpPort, startMcpHttpServer } = await import("./http.ts")
423
+
424
+ if (isHttpMode()) {
425
+ const handle = await startMcpHttpServer(buildServer, {
426
+ port: resolveMcpHttpPort(),
427
+ })
428
+ process.on("SIGINT", () => void handle.close().finally(() => process.exit(0)))
429
+ process.on("SIGTERM", () => void handle.close().finally(() => process.exit(0)))
430
+ return
431
+ }
432
+
433
+ const server = buildServer()
434
+ const transport = new StdioServerTransport()
435
+ await server.connect(transport)
436
+ }
437
+
438
+ if (import.meta.main) {
439
+ main().catch((err) => {
440
+ console.error(err)
441
+ process.exit(1)
442
+ })
443
+ }