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 +21 -0
- package/README.md +108 -0
- package/dist/auth.d.ts +9 -0
- package/dist/auth.js +80 -0
- package/dist/auth.js.map +1 -0
- package/dist/config.d.ts +14 -0
- package/dist/config.js +94 -0
- package/dist/config.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +62 -0
- package/dist/index.js.map +1 -0
- package/dist/logger.d.ts +11 -0
- package/dist/logger.js +40 -0
- package/dist/logger.js.map +1 -0
- package/dist/mcp.d.ts +17 -0
- package/dist/mcp.js +174 -0
- package/dist/mcp.js.map +1 -0
- package/dist/proxy.d.ts +6 -0
- package/dist/proxy.js +124 -0
- package/dist/proxy.js.map +1 -0
- package/dist/session.d.ts +14 -0
- package/dist/session.js +66 -0
- package/dist/session.js.map +1 -0
- package/dist/types.d.ts +13 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +50 -0
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
|
package/dist/auth.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"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"}
|
package/dist/config.d.ts
ADDED
|
@@ -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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"}
|
package/dist/index.d.ts
ADDED
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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"}
|
package/dist/logger.d.ts
ADDED
|
@@ -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
|
package/dist/mcp.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mcp.js","sourceRoot":"","sources":["../src/mcp.ts"],"names":[],"mappings":"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"}
|
package/dist/proxy.d.ts
ADDED
|
@@ -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
|
+
}
|
package/dist/session.js
ADDED
|
@@ -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":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AAIpC,MAAM,OAAO,cAAc;IACjB,OAAO,GAAuB,IAAI,CAAC;IACnC,KAAK,GAAG,IAAI,KAAK,EAAE,CAAC;IACpB,WAAW,CAAS;IACpB,MAAM,CAAS;IAEvB,YAAY,WAAmB,EAAE,MAAc;QAC7C,IAAI,CAAC,WAAW,GAAG,WAAW,CAAC;QAC/B,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;IACvB,CAAC;IAED,UAAU;QACR,OAAO,IAAI,CAAC,OAAO,CAAC;IACtB,CAAC;IAED,YAAY;QACV,IAAI,CAAC;YACH,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,WAAW,CAAC;gBAAE,OAAO,KAAK,CAAC;YAEnD,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC,CAAC;YACnE,IAAI,GAAG,CAAC,cAAc,IAAI,GAAG,CAAC,oBAAoB,EAAE,CAAC;gBACnD,IAAI,CAAC,OAAO,GAAG;oBACb,cAAc,EAAE,GAAG,CAAC,cAAc;oBAClC,oBAAoB,EAAE,GAAG,CAAC,oBAAoB;iBAC/C,CAAC;gBAEF,MAAM,SAAS,GAAG,IAAI,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,oBAAoB,GAAG,IAAI,CAAC,CAAC,WAAW,EAAE,CAAC;gBACnF,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,8CAA8C,SAAS,GAAG,CAAC,CAAC;gBAC7E,OAAO,IAAI,CAAC;YACd,CAAC;QACH,CAAC;QAAC,MAAM,CAAC;YACP,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,kCAAkC,CAAC,CAAC;QACvD,CAAC;QACD,OAAO,KAAK,CAAC;IACf,CAAC;IAED,KAAK,CAAC,kBAAkB,CACtB,YAAwC;QAExC,OAAO,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,KAAK,IAAI,EAAE;YACxC,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;gBACjB,OAAO,IAAI,CAAC,OAAO,CAAC;YACtB,CAAC;YAED,IAAI,CAAC,OAAO,GAAG,MAAM,YAAY,EAAE,CAAC;YACpC,IAAI,CAAC,UAAU,EAAE,CAAC;YAClB,OAAO,IAAI,CAAC,OAAO,CAAC;QACtB,CAAC,CAAC,CAAC;IACL,CAAC;IAED,KAAK,CAAC,YAAY;QAChB,OAAO,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,KAAK,IAAI,EAAE;YACxC,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC;YACpB,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,iBAAiB,CAAC,CAAC;QACtC,CAAC,CAAC,CAAC;IACL,CAAC;IAED,UAAU;QACR,IAAI,CAAC,IAAI,CAAC,OAAO;YAAE,OAAO;QAE1B,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;YAC3C,EAAE,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;YACvC,EAAE,CAAC,aAAa,CACd,IAAI,CAAC,WAAW,EAChB,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,EACrC,EAAE,IAAI,EAAE,KAAK,EAAE,CAChB,CAAC;YACF,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,oBAAoB,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC;QAC5D,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,2BAA2B,GAAG,EAAE,CAAC,CAAC;QACtD,CAAC;IACH,CAAC;CACF"}
|
package/dist/types.d.ts
ADDED
|
@@ -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 @@
|
|
|
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
|
+
}
|