@hasna/logs 0.3.23 → 0.3.24
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 +1 -2
- package/README.md +3 -22
- package/bun.lock +7 -14
- package/dist/cli/index.js +2 -2
- package/dist/{index-t97ttm0a.js → index-75dwghvv.js} +88 -6
- package/dist/{index-1f2ghyhm.js → index-g8f8kep6.js} +93 -8
- package/dist/{index-zmayq5kj.js → index-pf8hpweg.js} +2 -2
- package/dist/{index-2sbhn1ye.js → index-w24zm361.js} +2 -2
- package/dist/mcp/index.js +23801 -8266
- package/dist/server/index.js +2 -2
- package/package.json +4 -3
- package/sdk/package.json +4 -9
- package/src/cli/entrypoints.test.ts +1 -1
- package/src/db/index.ts +1 -1
- package/src/lib/cloud-sync.ts +167 -0
- package/src/lib/ingest.ts +3 -3
- package/src/lib/remote-storage.ts +45 -0
- package/src/mcp/index.ts +30 -34
- package/dist/export-yjaar93b.js +0 -10
- package/dist/health-9792c1rc.js +0 -8
- package/dist/health-egdb00st.js +0 -8
- package/dist/http-0wsh40x1.js +0 -1240
- package/dist/index-14dvwcf1.js +0 -45
- package/dist/index-4ba0sabv.js +0 -1241
- package/dist/index-4hj4sakk.js +0 -1241
- package/dist/index-5cj74qka.js +0 -10803
- package/dist/index-5qwba140.js +0 -1241
- package/dist/index-5tvnhvgr.js +0 -536
- package/dist/index-6y8pmes4.js +0 -45
- package/dist/index-6zrkek5y.js +0 -9454
- package/dist/index-7qhh666n.js +0 -1241
- package/dist/index-86j0hn03.js +0 -540
- package/dist/index-exeq2gs6.js +0 -79
- package/dist/index-fzmz9aqs.js +0 -1241
- package/dist/index-g8dczzvv.js +0 -30
- package/dist/index-hjzbctgt.js +0 -5868
- package/dist/index-rbrsvsyh.js +0 -88
- package/dist/index-vmr85wa1.js +0 -9579
- package/dist/index-wbsq8qjd.js +0 -1241
- package/dist/index-xjn8gam3.js +0 -39
- package/dist/index-yb8yd4j6.js +0 -39
- package/dist/jobs-02z4fzsn.js +0 -22
- package/dist/query-6s5gqkck.js +0 -15
- package/dist/query-shjjj67k.js +0 -14
- package/dist/query-tcg3bm9s.js +0 -14
- package/src/mcp/http.test.ts +0 -92
- package/src/mcp/http.ts +0 -135
package/dist/server/index.js
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
setPageAuth,
|
|
7
7
|
setRetentionPolicy,
|
|
8
8
|
startScheduler
|
|
9
|
-
} from "../index-
|
|
9
|
+
} from "../index-pf8hpweg.js";
|
|
10
10
|
import {
|
|
11
11
|
exportToCsv,
|
|
12
12
|
exportToJson
|
|
@@ -37,7 +37,7 @@ import {
|
|
|
37
37
|
updateAlertRule,
|
|
38
38
|
updateIssueStatus,
|
|
39
39
|
updateProject
|
|
40
|
-
} from "../index-
|
|
40
|
+
} from "../index-g8f8kep6.js";
|
|
41
41
|
import {
|
|
42
42
|
createJob,
|
|
43
43
|
deleteJob,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hasna/logs",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.24",
|
|
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",
|
|
@@ -40,12 +40,12 @@
|
|
|
40
40
|
"author": "Andrei Hasna <andrei@hasna.com>",
|
|
41
41
|
"license": "Apache-2.0",
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@
|
|
44
|
-
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
43
|
+
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
45
44
|
"commander": "^14.0.0",
|
|
46
45
|
"hono": "^4.7.11",
|
|
47
46
|
"ink": "^5.1.0",
|
|
48
47
|
"node-cron": "^3.0.3",
|
|
48
|
+
"pg": "^8.20.0",
|
|
49
49
|
"playwright": "^1.52.0",
|
|
50
50
|
"react": "^19.1.0"
|
|
51
51
|
},
|
|
@@ -53,6 +53,7 @@
|
|
|
53
53
|
"@biomejs/biome": "^1.9.4",
|
|
54
54
|
"@types/bun": "latest",
|
|
55
55
|
"@types/node-cron": "^3.0.11",
|
|
56
|
+
"@types/pg": "^8.20.0",
|
|
56
57
|
"@types/react": "^19.1.4",
|
|
57
58
|
"typescript": "^5.9.3"
|
|
58
59
|
}
|
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
|
|
4
|
+
"description": "Zero-dependency fetch client for @hasna/logs — push logs, query, browse issues, perf snapshots",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
7
7
|
"types": "./dist/index.d.ts",
|
|
@@ -10,18 +10,13 @@
|
|
|
10
10
|
"./browser": "./dist/index.js"
|
|
11
11
|
},
|
|
12
12
|
"publishConfig": {
|
|
13
|
-
"access": "
|
|
13
|
+
"access": "restricted",
|
|
14
14
|
"registry": "https://registry.npmjs.org/"
|
|
15
15
|
},
|
|
16
16
|
"scripts": {
|
|
17
17
|
"build": "bun build src/index.ts --outdir dist --target browser"
|
|
18
18
|
},
|
|
19
|
-
"keywords": [
|
|
20
|
-
"logs",
|
|
21
|
-
"monitoring",
|
|
22
|
-
"sdk",
|
|
23
|
-
"ai-agents"
|
|
24
|
-
],
|
|
19
|
+
"keywords": ["logs", "monitoring", "sdk", "ai-agents"],
|
|
25
20
|
"author": "Andrei Hasna <andrei@hasna.com>",
|
|
26
|
-
"license": "
|
|
21
|
+
"license": "MIT"
|
|
27
22
|
}
|
|
@@ -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
|
|
49
|
+
expect(result.stdout).toContain("Start the @hasna/logs MCP server over stdio.")
|
|
50
50
|
expect(result.stdout).not.toContain("Listening")
|
|
51
51
|
expect(result.stderr.trim()).toBe("")
|
|
52
52
|
})
|
package/src/db/index.ts
CHANGED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import type { Database } from "bun:sqlite"
|
|
2
|
+
import { getDb } from "../db/index.ts"
|
|
3
|
+
import { PG_MIGRATIONS } from "../db/pg-migrations.ts"
|
|
4
|
+
import { PgAdapterAsync } from "./remote-storage.ts"
|
|
5
|
+
|
|
6
|
+
export const CLOUD_TABLES = [
|
|
7
|
+
"projects",
|
|
8
|
+
"pages",
|
|
9
|
+
"logs",
|
|
10
|
+
"scan_jobs",
|
|
11
|
+
"scan_runs",
|
|
12
|
+
"performance_snapshots",
|
|
13
|
+
"alert_rules",
|
|
14
|
+
"issues",
|
|
15
|
+
"page_auth",
|
|
16
|
+
"feedback",
|
|
17
|
+
] as const
|
|
18
|
+
|
|
19
|
+
type Row = Record<string, unknown>
|
|
20
|
+
|
|
21
|
+
export function getCloudDatabaseUrl(): string | null {
|
|
22
|
+
return process.env.HASNA_LOGS_CLOUD_DATABASE_URL
|
|
23
|
+
?? process.env.LOGS_CLOUD_DATABASE_URL
|
|
24
|
+
?? null
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function getCloudPg(): Promise<PgAdapterAsync> {
|
|
28
|
+
const url = getCloudDatabaseUrl()
|
|
29
|
+
if (!url) throw new Error("Missing HASNA_LOGS_CLOUD_DATABASE_URL or LOGS_CLOUD_DATABASE_URL")
|
|
30
|
+
return new PgAdapterAsync(url)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function runCloudMigrations(remote: PgAdapterAsync): Promise<void> {
|
|
34
|
+
for (const sql of PG_MIGRATIONS) await remote.run(sql)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function cloudPush(opts?: { tables?: string[] }): Promise<{ rows: number }> {
|
|
38
|
+
const remote = await getCloudPg()
|
|
39
|
+
try {
|
|
40
|
+
await runCloudMigrations(remote)
|
|
41
|
+
const db = getDb()
|
|
42
|
+
let rows = 0
|
|
43
|
+
for (const table of resolveTables(opts?.tables)) rows += await pushTable(db, remote, table)
|
|
44
|
+
return { rows }
|
|
45
|
+
} finally {
|
|
46
|
+
await remote.close()
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function cloudPull(opts?: { tables?: string[] }): Promise<{ rows: number }> {
|
|
51
|
+
const remote = await getCloudPg()
|
|
52
|
+
try {
|
|
53
|
+
await runCloudMigrations(remote)
|
|
54
|
+
const db = getDb()
|
|
55
|
+
let rows = 0
|
|
56
|
+
for (const table of resolveTables(opts?.tables)) rows += await pullTable(remote, db, table)
|
|
57
|
+
return { rows }
|
|
58
|
+
} finally {
|
|
59
|
+
await remote.close()
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export async function cloudSync(opts?: { tables?: string[] }): Promise<{ push: number; pull: number }> {
|
|
64
|
+
const push = await cloudPush(opts)
|
|
65
|
+
const pull = await cloudPull(opts)
|
|
66
|
+
return { push: push.rows, pull: pull.rows }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function resolveTables(tables?: string[]): string[] {
|
|
70
|
+
if (!tables || tables.length === 0) return [...CLOUD_TABLES]
|
|
71
|
+
const allowed = new Set<string>(CLOUD_TABLES)
|
|
72
|
+
const requested = tables.map(table => table.trim()).filter(Boolean)
|
|
73
|
+
const invalid = requested.filter(table => !allowed.has(table))
|
|
74
|
+
if (invalid.length > 0) throw new Error(`Unknown logs sync table(s): ${invalid.join(", ")}`)
|
|
75
|
+
return requested
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function quoteIdent(identifier: string): string {
|
|
79
|
+
return `"${identifier.replace(/"/g, "\"\"")}"`
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function pushTable(db: Database, remote: PgAdapterAsync, table: string): Promise<number> {
|
|
83
|
+
const rows = db.query(`SELECT * FROM ${quoteIdent(table)}`).all() as Row[]
|
|
84
|
+
if (rows.length === 0) return 0
|
|
85
|
+
const columns = await filterRemoteColumns(remote, table, Object.keys(rows[0]!))
|
|
86
|
+
await upsertPg(remote, table, columns, rows)
|
|
87
|
+
return rows.length
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function pullTable(remote: PgAdapterAsync, db: Database, table: string): Promise<number> {
|
|
91
|
+
const rows = await remote.all(`SELECT * FROM ${quoteIdent(table)}`) as Row[]
|
|
92
|
+
if (rows.length === 0) return 0
|
|
93
|
+
const columns = filterLocalColumns(db, table, Object.keys(rows[0]!))
|
|
94
|
+
upsertSqlite(db, table, columns, rows)
|
|
95
|
+
return rows.length
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function filterRemoteColumns(remote: PgAdapterAsync, table: string, columns: string[]): Promise<string[]> {
|
|
99
|
+
const rows = await remote.all(`
|
|
100
|
+
SELECT column_name
|
|
101
|
+
FROM information_schema.columns
|
|
102
|
+
WHERE table_schema = 'public' AND table_name = ?
|
|
103
|
+
`, table) as Array<{ column_name: string }>
|
|
104
|
+
if (rows.length === 0) return columns
|
|
105
|
+
const allowed = new Set(rows.map(row => row.column_name))
|
|
106
|
+
return columns.filter(column => allowed.has(column))
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function filterLocalColumns(db: Database, table: string, columns: string[]): string[] {
|
|
110
|
+
const rows = db.query(`PRAGMA table_info(${quoteIdent(table)})`).all() as Array<{ name: string }>
|
|
111
|
+
const allowed = new Set(rows.map(row => row.name))
|
|
112
|
+
return columns.filter(column => allowed.has(column))
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function upsertPg(remote: PgAdapterAsync, table: string, columns: string[], rows: Row[]): Promise<void> {
|
|
116
|
+
if (columns.length === 0) return
|
|
117
|
+
const columnList = columns.map(quoteIdent).join(", ")
|
|
118
|
+
const updateColumns = columns.filter(column => column !== "id")
|
|
119
|
+
const setClause = updateColumns.length > 0
|
|
120
|
+
? updateColumns.map(column => `${quoteIdent(column)} = EXCLUDED.${quoteIdent(column)}`).join(", ")
|
|
121
|
+
: `${quoteIdent("id")} = EXCLUDED.${quoteIdent("id")}`
|
|
122
|
+
|
|
123
|
+
for (const row of rows) {
|
|
124
|
+
const placeholders = columns.map((_, index) => `$${index + 1}`).join(", ")
|
|
125
|
+
const params = columns.map(column => coerceForPg(table, column, row[column]))
|
|
126
|
+
await remote.run(
|
|
127
|
+
`INSERT INTO ${quoteIdent(table)} (${columnList}) VALUES (${placeholders})
|
|
128
|
+
ON CONFLICT (${quoteIdent("id")}) DO UPDATE SET ${setClause}`,
|
|
129
|
+
...params,
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function upsertSqlite(db: Database, table: string, columns: string[], rows: Row[]): void {
|
|
135
|
+
if (columns.length === 0) return
|
|
136
|
+
const columnList = columns.map(quoteIdent).join(", ")
|
|
137
|
+
const placeholders = columns.map(() => "?").join(", ")
|
|
138
|
+
const updateColumns = columns.filter(column => column !== "id")
|
|
139
|
+
const setClause = updateColumns.length > 0
|
|
140
|
+
? updateColumns.map(column => `${quoteIdent(column)} = excluded.${quoteIdent(column)}`).join(", ")
|
|
141
|
+
: `${quoteIdent("id")} = excluded.${quoteIdent("id")}`
|
|
142
|
+
const statement = db.prepare(
|
|
143
|
+
`INSERT INTO ${quoteIdent(table)} (${columnList}) VALUES (${placeholders})
|
|
144
|
+
ON CONFLICT (${quoteIdent("id")}) DO UPDATE SET ${setClause}`,
|
|
145
|
+
)
|
|
146
|
+
const insert = db.transaction((batch: Row[]) => {
|
|
147
|
+
for (const row of batch) statement.run(...columns.map(column => coerceForSqlite(row[column])))
|
|
148
|
+
})
|
|
149
|
+
insert(rows)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function coerceForPg(table: string, column: string, value: unknown): unknown {
|
|
153
|
+
if (value === undefined) return null
|
|
154
|
+
if ((table === "scan_jobs" || table === "alert_rules") && column === "enabled") {
|
|
155
|
+
return Boolean(value)
|
|
156
|
+
}
|
|
157
|
+
return value
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function coerceForSqlite(value: unknown): string | number | bigint | boolean | null | Uint8Array {
|
|
161
|
+
if (value === undefined || value === null) return null
|
|
162
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "bigint" || typeof value === "boolean") return value
|
|
163
|
+
if (value instanceof Date) return value.toISOString()
|
|
164
|
+
if (Buffer.isBuffer(value) || value instanceof Uint8Array) return value
|
|
165
|
+
if (typeof value === "object") return JSON.stringify(value)
|
|
166
|
+
return String(value)
|
|
167
|
+
}
|
package/src/lib/ingest.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { DbAdapter } from "
|
|
1
|
+
import type { Database as DbAdapter } from "bun:sqlite"
|
|
2
2
|
import type { LogEntry, LogRow } from "../types/index.ts"
|
|
3
3
|
import { upsertIssue } from "./issues.ts"
|
|
4
4
|
import { evaluateAlerts } from "./alerts.ts"
|
|
@@ -47,7 +47,7 @@ export function ingestBatch(db: DbAdapter, entries: LogEntry[], sharedTraceId?:
|
|
|
47
47
|
VALUES ($project_id, $page_id, $level, $source, $service, $message, $trace_id, $session_id, $agent, $url, $stack_trace, $metadata)
|
|
48
48
|
RETURNING *
|
|
49
49
|
`)
|
|
50
|
-
//
|
|
50
|
+
// Bun returns a callable transaction wrapper; invoke it to execute the batch.
|
|
51
51
|
const rows = db.transaction(() =>
|
|
52
52
|
entries.map(entry =>
|
|
53
53
|
insert.get({
|
|
@@ -65,7 +65,7 @@ export function ingestBatch(db: DbAdapter, entries: LogEntry[], sharedTraceId?:
|
|
|
65
65
|
$metadata: entry.metadata ? JSON.stringify(entry.metadata) : null,
|
|
66
66
|
}) as LogRow
|
|
67
67
|
)
|
|
68
|
-
)
|
|
68
|
+
)()
|
|
69
69
|
|
|
70
70
|
// Issue grouping for error-level entries (outside transaction for perf)
|
|
71
71
|
for (const entry of entries) {
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import pg from "pg"
|
|
2
|
+
import type { Pool } from "pg"
|
|
3
|
+
|
|
4
|
+
function translatePlaceholders(sql: string): string {
|
|
5
|
+
let index = 0
|
|
6
|
+
return sql.replace(/\?/g, () => `$${++index}`)
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function normalizeParams(params: unknown[]): unknown[] {
|
|
10
|
+
const flat = params.length === 1 && Array.isArray(params[0]) ? params[0] : params
|
|
11
|
+
return flat.map(value => value === undefined ? null : value)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function sslConfigFor(connectionString: string): { rejectUnauthorized: boolean } | undefined {
|
|
15
|
+
return connectionString.includes("sslmode=require") || connectionString.includes("ssl=true")
|
|
16
|
+
? { rejectUnauthorized: false }
|
|
17
|
+
: undefined
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class PgAdapterAsync {
|
|
21
|
+
private readonly pool: Pool
|
|
22
|
+
|
|
23
|
+
constructor(connectionString: string) {
|
|
24
|
+
this.pool = new pg.Pool({ connectionString, ssl: sslConfigFor(connectionString) })
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async run(sql: string, ...params: unknown[]): Promise<{ changes: number }> {
|
|
28
|
+
const result = await this.pool.query(translatePlaceholders(sql), normalizeParams(params))
|
|
29
|
+
return { changes: result.rowCount ?? 0 }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async get(sql: string, ...params: unknown[]): Promise<unknown> {
|
|
33
|
+
const result = await this.pool.query(translatePlaceholders(sql), normalizeParams(params))
|
|
34
|
+
return result.rows[0] ?? null
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async all(sql: string, ...params: unknown[]): Promise<unknown[]> {
|
|
38
|
+
const result = await this.pool.query(translatePlaceholders(sql), normalizeParams(params))
|
|
39
|
+
return result.rows
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async close(): Promise<void> {
|
|
43
|
+
await this.pool.end()
|
|
44
|
+
}
|
|
45
|
+
}
|
package/src/mcp/index.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"
|
|
3
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
4
|
-
import { registerCloudTools } from "@hasna/cloud"
|
|
5
4
|
import { z } from "zod"
|
|
6
5
|
import { getDb } from "../db/index.ts"
|
|
7
6
|
import { exitIfMetadataRequest, PACKAGE_VERSION } from "../lib/package-meta.ts"
|
|
@@ -20,26 +19,21 @@ import { compare } from "../lib/compare.ts"
|
|
|
20
19
|
import { getHealth } from "../lib/health.ts"
|
|
21
20
|
import { getSessionContext } from "../lib/session-context.ts"
|
|
22
21
|
import { parseTime } from "../lib/parse-time.ts"
|
|
22
|
+
import { cloudPull, cloudPush, cloudSync, getCloudDatabaseUrl, getCloudPg } from "../lib/cloud-sync.ts"
|
|
23
23
|
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
|
|
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
|
-
],
|
|
27
|
+
description: "Start the @hasna/logs MCP server over stdio.",
|
|
32
28
|
})
|
|
33
29
|
|
|
34
30
|
const db = getDb()
|
|
31
|
+
const server = new McpServer({ name: "logs", version: PACKAGE_VERSION })
|
|
35
32
|
|
|
36
|
-
// --- in-memory agent registry
|
|
33
|
+
// --- in-memory agent registry ---
|
|
37
34
|
interface _LogsAgent { id: string; name: string; session_id?: string; last_seen_at: string; project_id?: string }
|
|
38
35
|
const _logsAgents = new Map<string, _LogsAgent>()
|
|
39
36
|
|
|
40
|
-
export function buildServer(): McpServer {
|
|
41
|
-
const server = new McpServer({ name: "logs", version: PACKAGE_VERSION })
|
|
42
|
-
|
|
43
37
|
const BRIEF_FIELDS: (keyof LogRow)[] = ["id", "timestamp", "level", "message", "service"]
|
|
44
38
|
|
|
45
39
|
function applyBrief(rows: LogRow[], brief = true): unknown[] {
|
|
@@ -414,31 +408,33 @@ server.tool("list_agents", "List all registered agents.", {}, async () => {
|
|
|
414
408
|
return { content: [{ type: "text" as const, text: JSON.stringify([..._logsAgents.values()]) }] }
|
|
415
409
|
})
|
|
416
410
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
}
|
|
411
|
+
server.tool("cloud_status", "Check configured logs PostgreSQL remote.", {}, async () => {
|
|
412
|
+
const url = getCloudDatabaseUrl()
|
|
413
|
+
if (!url) return { content: [{ type: "text" as const, text: "cloud: not configured" }] }
|
|
414
|
+
let cloud: Awaited<ReturnType<typeof getCloudPg>> | null = null
|
|
415
|
+
try {
|
|
416
|
+
cloud = await getCloudPg()
|
|
417
|
+
await cloud.get("SELECT 1 as ok")
|
|
418
|
+
const tables = await cloud.all("SELECT tablename FROM pg_tables WHERE schemaname = 'public' ORDER BY tablename") as Array<{ tablename: string }>
|
|
419
|
+
return { content: [{ type: "text" as const, text: JSON.stringify({ connected: true, tables: tables.map(row => row.tablename) }) }] }
|
|
420
|
+
} catch (error) {
|
|
421
|
+
return { content: [{ type: "text" as const, text: String(error) }], isError: true }
|
|
422
|
+
} finally {
|
|
423
|
+
if (cloud) await cloud.close().catch(() => {})
|
|
424
|
+
}
|
|
425
|
+
})
|
|
420
426
|
|
|
421
|
-
|
|
422
|
-
|
|
427
|
+
server.tool("cloud_push", "Push local logs data to PostgreSQL.", { tables: z.array(z.string()).optional() }, async ({ tables }) => {
|
|
428
|
+
return { content: [{ type: "text" as const, text: JSON.stringify(await cloudPush({ tables })) }] }
|
|
429
|
+
})
|
|
423
430
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
await server.connect(transport)
|
|
428
|
-
return
|
|
429
|
-
}
|
|
431
|
+
server.tool("cloud_pull", "Pull logs data from PostgreSQL.", { tables: z.array(z.string()).optional() }, async ({ tables }) => {
|
|
432
|
+
return { content: [{ type: "text" as const, text: JSON.stringify(await cloudPull({ tables })) }] }
|
|
433
|
+
})
|
|
430
434
|
|
|
431
|
-
|
|
432
|
-
const
|
|
433
|
-
|
|
434
|
-
})
|
|
435
|
-
process.on("SIGINT", () => void handle.close().finally(() => process.exit(0)))
|
|
436
|
-
process.on("SIGTERM", () => void handle.close().finally(() => process.exit(0)))
|
|
437
|
-
}
|
|
435
|
+
server.tool("cloud_sync", "Push local logs data, then pull remote rows.", { tables: z.array(z.string()).optional() }, async ({ tables }) => {
|
|
436
|
+
return { content: [{ type: "text" as const, text: JSON.stringify(await cloudSync({ tables })) }] }
|
|
437
|
+
})
|
|
438
438
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
console.error(err)
|
|
442
|
-
process.exit(1)
|
|
443
|
-
})
|
|
444
|
-
}
|
|
439
|
+
const transport = new StdioServerTransport()
|
|
440
|
+
await server.connect(transport)
|
package/dist/export-yjaar93b.js
DELETED
package/dist/health-9792c1rc.js
DELETED