grafana-bridge 0.1.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 grafana-bridge contributors
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,108 @@
1
+ # grafana-bridge
2
+
3
+ Local HTTP proxy that bridges CLI tools, scripts, and AI-powered dashboards to Grafana Cloud instances behind SSO (Okta, Google, Azure AD, etc.).
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npx grafana-bridge --grafana-url https://mycompany.grafana.net
9
+ ```
10
+
11
+ Or install globally:
12
+
13
+ ```bash
14
+ npm install -g grafana-bridge
15
+ grafana-bridge -u https://mycompany.grafana.net
16
+ ```
17
+
18
+ Requires Node.js >= 18. Playwright's Chromium is installed automatically via `postinstall`.
19
+
20
+ ## Usage
21
+
22
+ ```bash
23
+ # Start the proxy
24
+ grafana-bridge -u https://mycompany.grafana.net
25
+
26
+ # With custom port
27
+ grafana-bridge -u https://mycompany.grafana.net -p 8080
28
+
29
+ # With verbose logging
30
+ grafana-bridge -u https://mycompany.grafana.net --verbose
31
+
32
+ # With config file
33
+ grafana-bridge -c ./my-config.yaml
34
+ ```
35
+
36
+ Once running, any HTTP client can query Grafana through the local proxy:
37
+
38
+ ```bash
39
+ # List datasources
40
+ curl http://localhost:4000/api/datasources
41
+
42
+ # Query Loki
43
+ curl -X POST http://localhost:4000/api/ds/query \
44
+ -H "Content-Type: application/json" \
45
+ -d '{"queries": [...]}'
46
+
47
+ # Search dashboards
48
+ curl http://localhost:4000/api/search
49
+
50
+ # Health check
51
+ curl http://localhost:4000/health
52
+
53
+ # Trigger proactive authentication
54
+ curl -X POST http://localhost:4000/auth
55
+ ```
56
+
57
+ ## Configuration
58
+
59
+ ### CLI flags
60
+
61
+ ```
62
+ grafana-bridge [options]
63
+
64
+ -u, --grafana-url <url> Grafana instance URL (required)
65
+ -p, --port <number> Local proxy port (default: 4000)
66
+ -c, --config <path> Path to config file
67
+ --login-timeout <ms> SSO login timeout (default: 120000)
68
+ --session-file <path> Session persistence file path
69
+ --verbose Enable debug logging
70
+ -v, --version
71
+ -h, --help
72
+ ```
73
+
74
+ ### Config file
75
+
76
+ Place at `~/.config/grafana-bridge/config.yaml` or `./grafana-bridge.yaml`:
77
+
78
+ ```yaml
79
+ grafanaUrl: 'https://mycompany.grafana.net'
80
+ port: 4000
81
+ loginTimeoutMs: 120000
82
+ sessionFile: '~/.config/grafana-bridge/session.json'
83
+ verbose: false
84
+ ```
85
+
86
+ ### Environment variables
87
+
88
+ - `GRAFANA_BRIDGE_URL` — Grafana instance URL
89
+ - `GRAFANA_BRIDGE_PORT` — Local proxy port
90
+
91
+ ### Precedence
92
+
93
+ CLI flags > environment variables > config file > defaults
94
+
95
+ ## How it works
96
+
97
+ 1. All requests are forwarded to Grafana with session cookies injected (if a session exists)
98
+ 2. If Grafana returns 401, a Chromium browser opens for SSO login (lazy authentication)
99
+ 3. After successful login, `grafana_session` and `grafana_session_expiry` cookies are captured
100
+ 4. Session is cached in memory and persisted to `~/.config/grafana-bridge/session.json`
101
+ 5. Subsequent requests reuse the cached session — the browser only opens when Grafana rejects the session
102
+ 6. Re-authentication retries up to 3 times before returning 401 to the client
103
+ 7. Persistent browser context means SSO IdP cookies are remembered — subsequent re-auths only need Grafana consent, not full IdP login
104
+ 8. `POST /auth` can be used to proactively trigger authentication before any request
105
+
106
+ ## License
107
+
108
+ MIT
package/dist/auth.d.ts ADDED
@@ -0,0 +1,9 @@
1
+ import type { SessionData } from "./types.js";
2
+ import type { Logger } from "./logger.js";
3
+ export declare class Authenticator {
4
+ private grafanaUrl;
5
+ private loginTimeoutMs;
6
+ private logger;
7
+ constructor(grafanaUrl: string, loginTimeoutMs: number, logger: Logger);
8
+ authenticate(): Promise<SessionData>;
9
+ }
package/dist/auth.js ADDED
@@ -0,0 +1,80 @@
1
+ import path from "node:path";
2
+ import os from "node:os";
3
+ import { chromium } from "playwright";
4
+ const BROWSER_DATA_DIR = path.join(os.homedir(), ".config", "grafana-bridge", "browser-data");
5
+ export class Authenticator {
6
+ grafanaUrl;
7
+ loginTimeoutMs;
8
+ logger;
9
+ constructor(grafanaUrl, loginTimeoutMs, logger) {
10
+ this.grafanaUrl = grafanaUrl;
11
+ this.loginTimeoutMs = loginTimeoutMs;
12
+ this.logger = logger;
13
+ }
14
+ async authenticate() {
15
+ this.logger.info("Opening browser for SSO login...");
16
+ const context = await chromium.launchPersistentContext(BROWSER_DATA_DIR, {
17
+ headless: false,
18
+ viewport: { width: 480, height: 640 },
19
+ });
20
+ try {
21
+ const page = context.pages()[0] ?? (await context.newPage());
22
+ try {
23
+ await page.goto(this.grafanaUrl);
24
+ }
25
+ catch (err) {
26
+ throw new Error(`Browser closed before authentication completed`);
27
+ }
28
+ return await new Promise((resolve, reject) => {
29
+ const timeout = setTimeout(() => {
30
+ cleanup();
31
+ context.close().catch(() => { });
32
+ reject(new Error(`SSO login timed out after ${this.loginTimeoutMs}ms`));
33
+ }, this.loginTimeoutMs);
34
+ context.on("close", () => {
35
+ cleanup();
36
+ clearTimeout(timeout);
37
+ reject(new Error("Browser closed before authentication completed"));
38
+ });
39
+ const checkCookies = async () => {
40
+ try {
41
+ const cookies = await context.cookies(this.grafanaUrl);
42
+ const session = cookies.find((c) => c.name === "grafana_session");
43
+ const expiry = cookies.find((c) => c.name === "grafana_session_expiry");
44
+ if (session?.value && expiry?.value) {
45
+ const expiryNum = parseInt(expiry.value, 10);
46
+ if (isNaN(expiryNum))
47
+ return;
48
+ if (expiryNum * 1000 > Date.now()) {
49
+ cleanup();
50
+ clearTimeout(timeout);
51
+ const expiresAt = new Date(expiryNum * 1000).toISOString();
52
+ this.logger.info("SSO login successful");
53
+ this.logger.info(`Session expires at ${expiresAt}`);
54
+ await page.close();
55
+ await context.close();
56
+ resolve({
57
+ grafanaSession: session.value,
58
+ grafanaSessionExpiry: expiryNum,
59
+ });
60
+ }
61
+ }
62
+ }
63
+ catch {
64
+ // Browser may be navigating, ignore transient errors
65
+ }
66
+ };
67
+ const cleanup = () => {
68
+ page.removeListener("framenavigated", checkCookies);
69
+ };
70
+ page.on("framenavigated", checkCookies);
71
+ checkCookies();
72
+ });
73
+ }
74
+ catch (err) {
75
+ await context.close().catch(() => { });
76
+ throw err;
77
+ }
78
+ }
79
+ }
80
+ //# sourceMappingURL=auth.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth.js","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,14 @@
1
+ import type { Config } from "./types.js";
2
+ export type CliFlags = Partial<{
3
+ instance: string;
4
+ grafanaUrl: string;
5
+ port: string;
6
+ config: string;
7
+ loginTimeout: string;
8
+ sessionFile: string;
9
+ verbose: boolean;
10
+ mcp: boolean;
11
+ contextFile: string;
12
+ }>;
13
+ export declare function resolveInstanceUrl(input: string): string;
14
+ export declare function resolveConfig(cli: CliFlags): Config;
package/dist/config.js ADDED
@@ -0,0 +1,94 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import os from "node:os";
4
+ import { parse as parseYaml } from "yaml";
5
+ const DEFAULT_PORT = 4000;
6
+ const DEFAULT_LOGIN_TIMEOUT_MS = 120_000;
7
+ const DEFAULT_CONFIG_PATHS = [
8
+ "./grafana-bridge.yaml",
9
+ path.join(os.homedir(), ".config", "grafana-bridge", "config.yaml"),
10
+ ];
11
+ function defaultSessionFile() {
12
+ return path.join(os.homedir(), ".config", "grafana-bridge", "session.json");
13
+ }
14
+ function loadYaml(configPath) {
15
+ const paths = configPath ? [configPath] : DEFAULT_CONFIG_PATHS;
16
+ for (const p of paths) {
17
+ const resolved = p.startsWith("~")
18
+ ? path.join(os.homedir(), p.slice(1))
19
+ : path.resolve(p);
20
+ if (fs.existsSync(resolved)) {
21
+ const content = fs.readFileSync(resolved, "utf-8");
22
+ return parseYaml(content) ?? {};
23
+ }
24
+ }
25
+ return {};
26
+ }
27
+ function loadEnv() {
28
+ const raw = {};
29
+ if (process.env.GRAFANA_BRIDGE_URL) {
30
+ raw.grafanaUrl = process.env.GRAFANA_BRIDGE_URL;
31
+ }
32
+ if (process.env.GRAFANA_BRIDGE_PORT) {
33
+ raw.port = parseInt(process.env.GRAFANA_BRIDGE_PORT, 10);
34
+ }
35
+ if (process.env.GRAFANA_BRIDGE_CONTEXT_FILE) {
36
+ raw.contextFile = process.env.GRAFANA_BRIDGE_CONTEXT_FILE;
37
+ }
38
+ return raw;
39
+ }
40
+ export function resolveInstanceUrl(input) {
41
+ try {
42
+ const url = new URL(input);
43
+ if (url.protocol === "http:" || url.protocol === "https:") {
44
+ return input;
45
+ }
46
+ }
47
+ catch {
48
+ // not a valid URL, treat as slug
49
+ }
50
+ return `https://${input}.grafana.net`;
51
+ }
52
+ export function resolveConfig(cli) {
53
+ const yaml = loadYaml(cli.config);
54
+ const env = loadEnv();
55
+ const instanceUrl = cli.instance
56
+ ? resolveInstanceUrl(cli.instance)
57
+ : undefined;
58
+ const grafanaUrl = instanceUrl ?? cli.grafanaUrl ?? env.grafanaUrl ?? yaml.grafanaUrl ?? "";
59
+ const port = (cli.port ? parseInt(cli.port, 10) : undefined) ??
60
+ env.port ??
61
+ yaml.port ??
62
+ DEFAULT_PORT;
63
+ const loginTimeoutMs = (cli.loginTimeout ? parseInt(cli.loginTimeout, 10) : undefined) ??
64
+ yaml.loginTimeoutMs ??
65
+ DEFAULT_LOGIN_TIMEOUT_MS;
66
+ const sessionFile = cli.sessionFile ?? yaml.sessionFile ?? defaultSessionFile();
67
+ const verbose = cli.verbose ?? yaml.verbose ?? false;
68
+ const mcp = cli.mcp ?? yaml.mcp ?? false;
69
+ const contextFileRaw = cli.contextFile ?? env.contextFile ?? yaml.contextFile;
70
+ if (!mcp && !grafanaUrl) {
71
+ console.error("Error: instance argument or --grafana-url is required (or set grafanaUrl in config / GRAFANA_BRIDGE_URL env var)");
72
+ process.exit(1);
73
+ }
74
+ if (isNaN(port) || port < 1 || port > 65535) {
75
+ console.error(`Error: invalid port "${cli.port ?? yaml.port}"`);
76
+ process.exit(1);
77
+ }
78
+ return {
79
+ grafanaUrl: grafanaUrl ? grafanaUrl.replace(/\/+$/, "") : "",
80
+ port,
81
+ loginTimeoutMs,
82
+ sessionFile: sessionFile.startsWith("~")
83
+ ? path.join(os.homedir(), sessionFile.slice(1))
84
+ : path.resolve(sessionFile),
85
+ verbose,
86
+ mcp,
87
+ contextFile: contextFileRaw
88
+ ? contextFileRaw.startsWith("~")
89
+ ? path.join(os.homedir(), contextFileRaw.slice(1))
90
+ : path.resolve(contextFileRaw)
91
+ : undefined,
92
+ };
93
+ }
94
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,62 @@
1
+ #!/usr/bin/env node
2
+ import fs from "node:fs";
3
+ import { Command } from "commander";
4
+ import { resolveConfig } from "./config.js";
5
+ import { Logger } from "./logger.js";
6
+ import { SessionManager } from "./session.js";
7
+ import { Authenticator } from "./auth.js";
8
+ import { createProxyServer } from "./proxy.js";
9
+ import { startMcpServer } from "./mcp.js";
10
+ const program = new Command();
11
+ program
12
+ .name("grafana-bridge")
13
+ .description("Local HTTP proxy that bridges CLI tools to Grafana Cloud instances behind SSO")
14
+ .version("0.1.0")
15
+ .argument("[instance]", "Grafana Cloud slug (e.g. 'mycompany') or full URL")
16
+ .option("-u, --grafana-url <url>", "Grafana instance URL")
17
+ .option("-p, --port <number>", "Local proxy port")
18
+ .option("-c, --config <path>", "Path to config file")
19
+ .option("--login-timeout <ms>", "SSO login timeout in ms")
20
+ .option("--session-file <path>", "Session persistence file path")
21
+ .option("--verbose", "Enable debug logging")
22
+ .option("--mcp", "Run as MCP server over stdio (for Claude Desktop)")
23
+ .option("--context-file <path>", "Markdown file with instructions for the LLM (MCP mode only)")
24
+ .action(async (instance, opts) => {
25
+ const config = resolveConfig({ ...opts, instance });
26
+ const logger = new Logger(config.verbose, config.mcp);
27
+ if (config.mcp) {
28
+ let instructions;
29
+ if (config.contextFile) {
30
+ if (!fs.existsSync(config.contextFile)) {
31
+ console.error(`Error: context file not found: ${config.contextFile}`);
32
+ process.exit(1);
33
+ }
34
+ instructions = fs.readFileSync(config.contextFile, "utf-8");
35
+ }
36
+ await startMcpServer(config.port, logger, config.grafanaUrl || undefined, instructions);
37
+ return;
38
+ }
39
+ const sessionManager = new SessionManager(config.sessionFile, logger);
40
+ const authenticator = new Authenticator(config.grafanaUrl, config.loginTimeoutMs, logger);
41
+ const hasSession = sessionManager.loadFromDisk();
42
+ if (!hasSession) {
43
+ logger.info("No saved session found, will authenticate when Grafana requires it");
44
+ }
45
+ const server = createProxyServer(config, sessionManager, authenticator, logger);
46
+ server.listen(config.port, () => {
47
+ console.log();
48
+ console.log(` grafana-bridge v0.1.0`);
49
+ console.log(` Proxy ready → http://localhost:${config.port} → ${config.grafanaUrl}`);
50
+ console.log();
51
+ });
52
+ const shutdown = () => {
53
+ logger.info("Shutting down...");
54
+ sessionManager.saveToDisk();
55
+ server.close(() => process.exit(0));
56
+ setTimeout(() => process.exit(1), 5000);
57
+ };
58
+ process.on("SIGINT", shutdown);
59
+ process.on("SIGTERM", shutdown);
60
+ });
61
+ program.parse();
62
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ export type LogLevel = "debug" | "info" | "warn" | "error";
2
+ export declare class Logger {
3
+ private stderrOnly;
4
+ private minLevel;
5
+ constructor(verbose: boolean, stderrOnly?: boolean);
6
+ debug(message: string): void;
7
+ info(message: string): void;
8
+ warn(message: string): void;
9
+ error(message: string): void;
10
+ private log;
11
+ }
package/dist/logger.js ADDED
@@ -0,0 +1,40 @@
1
+ const LEVEL_PRIORITY = {
2
+ debug: 0,
3
+ info: 1,
4
+ warn: 2,
5
+ error: 3,
6
+ };
7
+ export class Logger {
8
+ stderrOnly;
9
+ minLevel;
10
+ constructor(verbose, stderrOnly = false) {
11
+ this.stderrOnly = stderrOnly;
12
+ this.minLevel = verbose ? "debug" : "info";
13
+ }
14
+ debug(message) {
15
+ this.log("debug", message);
16
+ }
17
+ info(message) {
18
+ this.log("info", message);
19
+ }
20
+ warn(message) {
21
+ this.log("warn", message);
22
+ }
23
+ error(message) {
24
+ this.log("error", message);
25
+ }
26
+ log(level, message) {
27
+ if (LEVEL_PRIORITY[level] < LEVEL_PRIORITY[this.minLevel])
28
+ return;
29
+ const timestamp = new Date().toISOString();
30
+ const label = level.toUpperCase().padEnd(5);
31
+ const output = `[${timestamp}] ${label} ${message}`;
32
+ if (this.stderrOnly || level === "error" || level === "warn") {
33
+ console.error(output);
34
+ }
35
+ else {
36
+ console.log(output);
37
+ }
38
+ }
39
+ }
40
+ //# sourceMappingURL=logger.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logger.js","sourceRoot":"","sources":["../src/logger.ts"],"names":[],"mappings":"AAEA,MAAM,cAAc,GAA6B;IAC/C,KAAK,EAAE,CAAC;IACR,IAAI,EAAE,CAAC;IACP,IAAI,EAAE,CAAC;IACP,KAAK,EAAE,CAAC;CACT,CAAC;AAEF,MAAM,OAAO,MAAM;IAGqB;IAF9B,QAAQ,CAAW;IAE3B,YAAY,OAAgB,EAAU,aAAa,KAAK;QAAlB,eAAU,GAAV,UAAU,CAAQ;QACtD,IAAI,CAAC,QAAQ,GAAG,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC;IAC7C,CAAC;IAED,KAAK,CAAC,OAAe;QACnB,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;IAC7B,CAAC;IAED,IAAI,CAAC,OAAe;QAClB,IAAI,CAAC,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC5B,CAAC;IAED,IAAI,CAAC,OAAe;QAClB,IAAI,CAAC,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC5B,CAAC;IAED,KAAK,CAAC,OAAe;QACnB,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;IAC7B,CAAC;IAEO,GAAG,CAAC,KAAe,EAAE,OAAe;QAC1C,IAAI,cAAc,CAAC,KAAK,CAAC,GAAG,cAAc,CAAC,IAAI,CAAC,QAAQ,CAAC;YAAE,OAAO;QAElE,MAAM,SAAS,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;QAC3C,MAAM,KAAK,GAAG,KAAK,CAAC,WAAW,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;QAC5C,MAAM,MAAM,GAAG,IAAI,SAAS,KAAK,KAAK,IAAI,OAAO,EAAE,CAAC;QAEpD,IAAI,IAAI,CAAC,UAAU,IAAI,KAAK,KAAK,OAAO,IAAI,KAAK,KAAK,MAAM,EAAE,CAAC;YAC7D,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;QACxB,CAAC;aAAM,CAAC;YACN,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;QACtB,CAAC;IACH,CAAC;CACF"}
package/dist/mcp.d.ts ADDED
@@ -0,0 +1,17 @@
1
+ import { z } from "zod";
2
+ import type { Logger } from "./logger.js";
3
+ type ProxyResponse = {
4
+ status: number;
5
+ body: string;
6
+ };
7
+ export declare function proxyFetch(port: number, method: string, path: string, body?: string): Promise<ProxyResponse>;
8
+ declare const QueryInput: z.ZodObject<{
9
+ datasourceUid: z.ZodString;
10
+ query: z.ZodString;
11
+ from: z.ZodDefault<z.ZodOptional<z.ZodString>>;
12
+ to: z.ZodDefault<z.ZodOptional<z.ZodString>>;
13
+ }, z.core.$strip>;
14
+ export declare function handleListDatasources(port: number, logger: Logger): Promise<string>;
15
+ export declare function handleQuery(port: number, logger: Logger, input: z.infer<typeof QueryInput>): Promise<string>;
16
+ export declare function startMcpServer(port: number, logger: Logger, grafanaUrl?: string, instructions?: string): Promise<void>;
17
+ export {};
package/dist/mcp.js ADDED
@@ -0,0 +1,174 @@
1
+ import http from "node:http";
2
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { ListToolsRequestSchema, CallToolRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
5
+ import { z } from "zod";
6
+ export function proxyFetch(port, method, path, body) {
7
+ return new Promise((resolve, reject) => {
8
+ const req = http.request({ hostname: "127.0.0.1", port, path, method, headers: body ? { "content-type": "application/json" } : {} }, (res) => {
9
+ const chunks = [];
10
+ res.on("data", (chunk) => chunks.push(chunk));
11
+ res.on("end", () => resolve({ status: res.statusCode, body: Buffer.concat(chunks).toString() }));
12
+ });
13
+ req.on("error", (err) => reject(err));
14
+ if (body)
15
+ req.write(body);
16
+ req.end();
17
+ });
18
+ }
19
+ function buildTools(grafanaUrl) {
20
+ const target = grafanaUrl ? ` on ${grafanaUrl}` : "";
21
+ return [
22
+ {
23
+ name: "list_datasources",
24
+ description: `List all configured Grafana datasources${target} with their name, UID, and type`,
25
+ inputSchema: {
26
+ type: "object",
27
+ properties: {},
28
+ required: [],
29
+ },
30
+ },
31
+ {
32
+ name: "query",
33
+ description: `Execute a query against a Grafana datasource${target}. Supports PostgreSQL (SQL), Prometheus (PromQL), and Loki (LogQL). ` +
34
+ "Automatically detects the datasource type from its UID and builds the correct query payload.",
35
+ inputSchema: {
36
+ type: "object",
37
+ properties: {
38
+ datasourceUid: {
39
+ type: "string",
40
+ description: "The UID of the datasource to query (from list_datasources)",
41
+ },
42
+ query: {
43
+ type: "string",
44
+ description: "The query string (SQL for PostgreSQL, PromQL for Prometheus, LogQL for Loki)",
45
+ },
46
+ from: {
47
+ type: "string",
48
+ description: "Start time (default: 'now-1h'). Accepts relative (now-1h) or absolute (ISO 8601) formats",
49
+ },
50
+ to: {
51
+ type: "string",
52
+ description: "End time (default: 'now'). Accepts relative (now) or absolute (ISO 8601) formats",
53
+ },
54
+ },
55
+ required: ["datasourceUid", "query"],
56
+ },
57
+ },
58
+ ];
59
+ }
60
+ const QueryInput = z.object({
61
+ datasourceUid: z.string(),
62
+ query: z.string(),
63
+ from: z.string().optional().default("now-1h"),
64
+ to: z.string().optional().default("now"),
65
+ });
66
+ function detectDatasourceType(typeName) {
67
+ const lower = typeName.toLowerCase();
68
+ if (lower.includes("postgres"))
69
+ return "postgres";
70
+ if (lower.includes("prometheus") || lower.includes("mimir"))
71
+ return "prometheus";
72
+ if (lower.includes("loki"))
73
+ return "loki";
74
+ return null;
75
+ }
76
+ function buildQueryPayload(datasourceUid, dsType, query, from, to) {
77
+ const base = { from, to, queries: [] };
78
+ const queries = base.queries;
79
+ switch (dsType) {
80
+ case "postgres":
81
+ queries.push({
82
+ refId: "A",
83
+ datasource: { uid: datasourceUid },
84
+ rawSql: query,
85
+ format: "table",
86
+ });
87
+ break;
88
+ case "prometheus":
89
+ queries.push({
90
+ refId: "A",
91
+ datasource: { uid: datasourceUid },
92
+ expr: query,
93
+ range: true,
94
+ instant: false,
95
+ });
96
+ break;
97
+ case "loki":
98
+ queries.push({
99
+ refId: "A",
100
+ datasource: { uid: datasourceUid },
101
+ expr: query,
102
+ queryType: "range",
103
+ });
104
+ break;
105
+ }
106
+ return base;
107
+ }
108
+ export async function handleListDatasources(port, logger) {
109
+ const res = await proxyFetch(port, "GET", "/api/datasources");
110
+ if (res.status !== 200) {
111
+ throw new Error(`Proxy returned status ${res.status}: ${res.body}`);
112
+ }
113
+ const datasources = JSON.parse(res.body);
114
+ return JSON.stringify(datasources.map((ds) => ({ name: ds.name, uid: ds.uid, type: ds.type })), null, 2);
115
+ }
116
+ export async function handleQuery(port, logger, input) {
117
+ const dsRes = await proxyFetch(port, "GET", `/api/datasources/uid/${input.datasourceUid}`);
118
+ if (dsRes.status !== 200) {
119
+ throw new Error(`Failed to fetch datasource ${input.datasourceUid}: status ${dsRes.status}`);
120
+ }
121
+ const dsData = JSON.parse(dsRes.body);
122
+ const dsType = detectDatasourceType(dsData.type);
123
+ if (!dsType) {
124
+ throw new Error(`Unsupported datasource type "${dsData.type}" for datasource "${dsData.name}". ` +
125
+ `Supported types: PostgreSQL, Prometheus, Loki`);
126
+ }
127
+ logger.debug(`Querying datasource "${dsData.name}" (type: ${dsData.type}, mapped: ${dsType})`);
128
+ const payload = buildQueryPayload(input.datasourceUid, dsType, input.query, input.from, input.to);
129
+ const queryRes = await proxyFetch(port, "POST", "/api/ds/query", JSON.stringify(payload));
130
+ if (queryRes.status !== 200) {
131
+ throw new Error(`Query failed with status ${queryRes.status}: ${queryRes.body}`);
132
+ }
133
+ return queryRes.body;
134
+ }
135
+ export async function startMcpServer(port, logger, grafanaUrl, instructions) {
136
+ const tools = buildTools(grafanaUrl);
137
+ const server = new Server({ name: "grafana-bridge", version: "0.1.0" }, { capabilities: { tools: {} }, instructions });
138
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
139
+ tools,
140
+ }));
141
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
142
+ const { name, arguments: args } = request.params;
143
+ try {
144
+ switch (name) {
145
+ case "list_datasources": {
146
+ const result = await handleListDatasources(port, logger);
147
+ return { content: [{ type: "text", text: result }] };
148
+ }
149
+ case "query": {
150
+ const input = QueryInput.parse(args);
151
+ const result = await handleQuery(port, logger, input);
152
+ return { content: [{ type: "text", text: result }] };
153
+ }
154
+ default:
155
+ return {
156
+ content: [{ type: "text", text: `Unknown tool: ${name}` }],
157
+ isError: true,
158
+ };
159
+ }
160
+ }
161
+ catch (error) {
162
+ const message = error instanceof Error ? error.message : String(error);
163
+ logger.error(`Tool "${name}" failed: ${message}`);
164
+ return {
165
+ content: [{ type: "text", text: `Error: ${message}` }],
166
+ isError: true,
167
+ };
168
+ }
169
+ });
170
+ const transport = new StdioServerTransport();
171
+ await server.connect(transport);
172
+ logger.info(`MCP server started (proxy: http://127.0.0.1:${port})`);
173
+ }
174
+ //# sourceMappingURL=mcp.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mcp.js","sourceRoot":"","sources":["../src/mcp.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ import http from "node:http";
2
+ import type { Config } from "./types.js";
3
+ import type { SessionManager } from "./session.js";
4
+ import type { Authenticator } from "./auth.js";
5
+ import type { Logger } from "./logger.js";
6
+ export declare function createProxyServer(config: Config, sessionManager: SessionManager, authenticator: Authenticator, logger: Logger): http.Server;
package/dist/proxy.js ADDED
@@ -0,0 +1,124 @@
1
+ import http from "node:http";
2
+ import https from "node:https";
3
+ const MAX_RETRIES = 3;
4
+ function buildCookieHeader(existing, session) {
5
+ const filtered = existing
6
+ ? existing
7
+ .split(";")
8
+ .map((c) => c.trim())
9
+ .filter((c) => !c.startsWith("grafana_session=") &&
10
+ !c.startsWith("grafana_session_expiry="))
11
+ : [];
12
+ filtered.push(`grafana_session=${session.grafanaSession}`);
13
+ filtered.push(`grafana_session_expiry=${session.grafanaSessionExpiry}`);
14
+ return filtered.join("; ");
15
+ }
16
+ function bufferBody(req) {
17
+ return new Promise((resolve, reject) => {
18
+ const chunks = [];
19
+ req.on("data", (chunk) => chunks.push(chunk));
20
+ req.on("end", () => resolve(Buffer.concat(chunks)));
21
+ req.on("error", reject);
22
+ });
23
+ }
24
+ async function forwardWithAuth(req, res, body, config, sessionManager, authenticator, logger, retryCount = 0) {
25
+ if (retryCount >= MAX_RETRIES) {
26
+ logger.error(`Auth failed after ${MAX_RETRIES} retries for ${req.method} ${req.url}`);
27
+ res.writeHead(401, { "Content-Type": "application/json" });
28
+ res.end(JSON.stringify({ error: "Authentication failed after retries" }));
29
+ return;
30
+ }
31
+ const session = sessionManager.getSession();
32
+ const targetUrl = new URL(req.url, config.grafanaUrl);
33
+ const isHttps = targetUrl.protocol === "https:";
34
+ const requester = isHttps ? https : http;
35
+ const headers = { ...req.headers, host: targetUrl.host };
36
+ delete headers["connection"];
37
+ if (session) {
38
+ headers.cookie = buildCookieHeader(headers.cookie, session);
39
+ }
40
+ else {
41
+ logger.debug("No session available, forwarding without auth cookies");
42
+ }
43
+ return new Promise((resolve) => {
44
+ const upstream = requester.request({
45
+ hostname: targetUrl.hostname,
46
+ port: targetUrl.port || (isHttps ? 443 : 80),
47
+ path: targetUrl.pathname + targetUrl.search,
48
+ method: req.method,
49
+ headers,
50
+ }, async (upstreamRes) => {
51
+ if (upstreamRes.statusCode === 401) {
52
+ upstreamRes.resume();
53
+ logger.warn(`Got 401 for ${req.method} ${req.url}, re-authenticating (retry ${retryCount + 1}/${MAX_RETRIES})`);
54
+ await sessionManager.clearSession();
55
+ try {
56
+ await sessionManager.ensureValidSession(() => authenticator.authenticate());
57
+ await forwardWithAuth(req, res, body, config, sessionManager, authenticator, logger, retryCount + 1);
58
+ }
59
+ catch (err) {
60
+ logger.error(`Re-authentication failed: ${err}`);
61
+ if (!res.headersSent) {
62
+ res.writeHead(401, { "Content-Type": "application/json" });
63
+ res.end(JSON.stringify({ error: "Authentication failed" }));
64
+ }
65
+ }
66
+ resolve();
67
+ return;
68
+ }
69
+ const responseHeaders = { ...upstreamRes.headers };
70
+ delete responseHeaders["transfer-encoding"];
71
+ logger.debug(`${req.method} ${req.url} -> ${upstreamRes.statusCode}`);
72
+ res.writeHead(upstreamRes.statusCode, responseHeaders);
73
+ upstreamRes.pipe(res);
74
+ upstreamRes.on("end", resolve);
75
+ upstreamRes.on("error", resolve);
76
+ });
77
+ upstream.on("error", (err) => {
78
+ logger.error(`Upstream request failed: ${err.message}`);
79
+ if (!res.headersSent) {
80
+ res.writeHead(502, { "Content-Type": "application/json" });
81
+ res.end(JSON.stringify({ error: "Failed to reach Grafana", details: err.message }));
82
+ }
83
+ resolve();
84
+ });
85
+ upstream.end(body);
86
+ });
87
+ }
88
+ export function createProxyServer(config, sessionManager, authenticator, logger) {
89
+ const server = http.createServer(async (req, res) => {
90
+ if (req.url === "/health") {
91
+ res.writeHead(200, { "Content-Type": "application/json" });
92
+ res.end(JSON.stringify({ status: "ok", target: config.grafanaUrl }));
93
+ return;
94
+ }
95
+ if (req.url === "/auth" && req.method === "POST") {
96
+ try {
97
+ const session = await sessionManager.ensureValidSession(() => authenticator.authenticate());
98
+ res.writeHead(200, { "Content-Type": "application/json" });
99
+ res.end(JSON.stringify({
100
+ status: "authenticated",
101
+ expiresAt: new Date(session.grafanaSessionExpiry * 1000).toISOString(),
102
+ }));
103
+ }
104
+ catch (err) {
105
+ res.writeHead(500, { "Content-Type": "application/json" });
106
+ res.end(JSON.stringify({ error: "Authentication failed", details: String(err) }));
107
+ }
108
+ return;
109
+ }
110
+ try {
111
+ const body = await bufferBody(req);
112
+ await forwardWithAuth(req, res, body, config, sessionManager, authenticator, logger);
113
+ }
114
+ catch (err) {
115
+ logger.error(`Unhandled error: ${err}`);
116
+ if (!res.headersSent) {
117
+ res.writeHead(500, { "Content-Type": "application/json" });
118
+ res.end(JSON.stringify({ error: "Internal proxy error" }));
119
+ }
120
+ }
121
+ });
122
+ return server;
123
+ }
124
+ //# sourceMappingURL=proxy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"proxy.js","sourceRoot":"","sources":["../src/proxy.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,14 @@
1
+ import type { SessionData } from "./types.js";
2
+ import type { Logger } from "./logger.js";
3
+ export declare class SessionManager {
4
+ private session;
5
+ private mutex;
6
+ private sessionFile;
7
+ private logger;
8
+ constructor(sessionFile: string, logger: Logger);
9
+ getSession(): SessionData | null;
10
+ loadFromDisk(): boolean;
11
+ ensureValidSession(authenticate: () => Promise<SessionData>): Promise<SessionData>;
12
+ clearSession(): Promise<void>;
13
+ saveToDisk(): void;
14
+ }
@@ -0,0 +1,66 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { Mutex } from "async-mutex";
4
+ export class SessionManager {
5
+ session = null;
6
+ mutex = new Mutex();
7
+ sessionFile;
8
+ logger;
9
+ constructor(sessionFile, logger) {
10
+ this.sessionFile = sessionFile;
11
+ this.logger = logger;
12
+ }
13
+ getSession() {
14
+ return this.session;
15
+ }
16
+ loadFromDisk() {
17
+ try {
18
+ if (!fs.existsSync(this.sessionFile))
19
+ return false;
20
+ const raw = JSON.parse(fs.readFileSync(this.sessionFile, "utf-8"));
21
+ if (raw.grafanaSession && raw.grafanaSessionExpiry) {
22
+ this.session = {
23
+ grafanaSession: raw.grafanaSession,
24
+ grafanaSessionExpiry: raw.grafanaSessionExpiry,
25
+ };
26
+ const expiresAt = new Date(this.session.grafanaSessionExpiry * 1000).toISOString();
27
+ this.logger.info(`Loaded session from disk (expiry metadata: ${expiresAt})`);
28
+ return true;
29
+ }
30
+ }
31
+ catch {
32
+ this.logger.warn("Failed to load session from disk");
33
+ }
34
+ return false;
35
+ }
36
+ async ensureValidSession(authenticate) {
37
+ return this.mutex.runExclusive(async () => {
38
+ if (this.session) {
39
+ return this.session;
40
+ }
41
+ this.session = await authenticate();
42
+ this.saveToDisk();
43
+ return this.session;
44
+ });
45
+ }
46
+ async clearSession() {
47
+ return this.mutex.runExclusive(async () => {
48
+ this.session = null;
49
+ this.logger.warn("Session cleared");
50
+ });
51
+ }
52
+ saveToDisk() {
53
+ if (!this.session)
54
+ return;
55
+ try {
56
+ const dir = path.dirname(this.sessionFile);
57
+ fs.mkdirSync(dir, { recursive: true });
58
+ fs.writeFileSync(this.sessionFile, JSON.stringify(this.session, null, 2), { mode: 0o600 });
59
+ this.logger.debug(`Session saved to ${this.sessionFile}`);
60
+ }
61
+ catch (err) {
62
+ this.logger.error(`Failed to save session: ${err}`);
63
+ }
64
+ }
65
+ }
66
+ //# sourceMappingURL=session.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session.js","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,13 @@
1
+ export type Config = {
2
+ grafanaUrl: string;
3
+ port: number;
4
+ loginTimeoutMs: number;
5
+ sessionFile: string;
6
+ verbose: boolean;
7
+ mcp: boolean;
8
+ contextFile?: string;
9
+ };
10
+ export type SessionData = {
11
+ grafanaSession: string;
12
+ grafanaSessionExpiry: number;
13
+ };
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "grafana-bridge",
3
+ "version": "0.1.0",
4
+ "description": "Local HTTP proxy that bridges CLI tools to Grafana Cloud instances behind SSO",
5
+ "type": "module",
6
+ "bin": {
7
+ "grafana-bridge": "./dist/index.js"
8
+ },
9
+ "files": [
10
+ "dist"
11
+ ],
12
+ "engines": {
13
+ "node": ">=18"
14
+ },
15
+ "scripts": {
16
+ "build": "tsc",
17
+ "dev": "tsx src/index.ts",
18
+ "test": "vitest run",
19
+ "test:watch": "vitest",
20
+ "prepublishOnly": "npm run build",
21
+ "postinstall": "npx playwright install chromium"
22
+ },
23
+ "keywords": [
24
+ "grafana",
25
+ "proxy",
26
+ "sso",
27
+ "authentication",
28
+ "cli"
29
+ ],
30
+ "license": "MIT",
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "https://github.com/molaga/grafana-bridge.git"
34
+ },
35
+ "dependencies": {
36
+ "@modelcontextprotocol/sdk": "^1.26.0",
37
+ "async-mutex": "^0.5.0",
38
+ "commander": "^12.1.0",
39
+ "playwright": "^1.49.0",
40
+ "yaml": "^2.6.0",
41
+ "zod": "^4.3.6"
42
+ },
43
+ "devDependencies": {
44
+ "@types/node": "^22.10.0",
45
+ "prettier": "^3.4.0",
46
+ "tsx": "^4.19.0",
47
+ "typescript": "^5.7.0",
48
+ "vitest": "^2.1.0"
49
+ }
50
+ }