@lacneu/atrium-mcp 0.30.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/README.md ADDED
@@ -0,0 +1,160 @@
1
+ # atrium-mcp
2
+
3
+ A thin, dependency-light **MCP server + CLI** that proxy the atrium
4
+ `/api/v1` observability surface using an `oc_live_` Bearer API key.
5
+
6
+ It speaks **HTTP only** — it imports nothing from the Convex app. Each tool maps
7
+ to one `/api/v1` route and one permission, which is enforced **server-side** by the
8
+ deployment (`requirePermission`). A scoped key (e.g. an `observer`) therefore simply
9
+ gets a `403` for routes it isn't allowed to call.
10
+
11
+ Runtime dependencies: `@modelcontextprotocol/sdk` and `zod` only.
12
+
13
+ ## Configuration (environment)
14
+
15
+ | Variable | Required | Default | Meaning |
16
+ | ---------------------------- | -------- | ------------------------ | -------------------------------------------------------- |
17
+ | `OPENCLAW_WEBCHAT_API_BASE` | no | `http://127.0.0.1:3213` | Deployment `.site` origin, **without** `/api/v1`. |
18
+ | `OPENCLAW_WEBCHAT_API_KEY` | **yes** | — | The `oc_live_...` Bearer key. |
19
+
20
+ The `/api/v1` prefix is added internally; you point `API_BASE` at the bare
21
+ `.site` origin. The key is only ever sent in the `Authorization` header — never
22
+ in a URL/query string and never logged.
23
+
24
+ ## Install (released artifact)
25
+
26
+ Published on every tagged release as **`@lacneu/atrium-mcp`** (npm, OIDC +
27
+ provenance), at the same lockstep version as the app images. Being HTTP-only and
28
+ provider-neutral, the same package serves any MCP-capable gateway (OpenClaw,
29
+ Hermes, Claude Code, …).
30
+
31
+ ```bash
32
+ # ad hoc (any MCP client with stdio command support)
33
+ npx -y @lacneu/atrium-mcp
34
+
35
+ # pinned, baked into a gateway image
36
+ npm install -g @lacneu/atrium-mcp@<version> # provides `atrium-mcp` + `atrium` (CLI)
37
+ ```
38
+
39
+ Example OpenClaw declaration (`mcp.servers`, stdio) — one server per deployment,
40
+ credentials injected via a wrapper or the process environment, never in config:
41
+
42
+ ```json
43
+ {
44
+ "mcp": {
45
+ "servers": {
46
+ "atrium-prod": { "command": "atrium-mcp-prod" }
47
+ }
48
+ }
49
+ }
50
+ ```
51
+
52
+ ## Build (from source)
53
+
54
+ ```bash
55
+ cd mcp
56
+ npm install
57
+ npm run build # emits dist/ (with shebangs preserved)
58
+ ```
59
+
60
+ Other scripts: `npm run typecheck`, `npm test`.
61
+
62
+ ## Tools (MCP) / Commands (CLI)
63
+
64
+ | MCP tool | CLI command | Route | Permission | Purpose |
65
+ | ---------------------- | ---------------- | ----------------------------- | ------------------ | ------- |
66
+ | `health` | `health` | `GET /api/v1/health` | none | Liveness probe for the API surface. |
67
+ | `get_compat` | `compat` | `GET /api/v1/compat` | `bridge.read` | Bridge version + per-instance gateway versions/capabilities. |
68
+ | `get_integrations` | — | `GET /api/v1/integrations` | `traces.read` | Opik/Langfuse configured/enabled + shipping cursors. No keys. |
69
+ | `get_chat_state` | — | `GET /api/v1/chat-state` | `traces.read` | Per-message lifecycle of one chat (metadata only). |
70
+ | `get_trace_enrichment` | — | `GET /api/v1/trace-enrichment` | `traces.read` | SOC2-safe span structure from Opik/Langfuse, keyed by `correlationId`. |
71
+ | `diagnose_chat` | — | `GET /api/v1/diagnose` | `traces.read` | One assessment of a chat + a suggested action/tool. |
72
+ | `reconcile_chat` | — | `POST /api/v1/reconcile-chat` | `selfheal` | Release a chat's stuck `streaming` message (text preserved). Audited. |
73
+ | `list_traces` | `traces` | `GET /api/v1/traces` | `traces.read` | Recent trace events, with filtering. |
74
+ | `get_kpi` | `kpi` | `GET /api/v1/kpi` | `kpi.read` | KPI rollups. |
75
+ | `query_openclaw` | `query-openclaw` | `POST /api/v1/openclaw/query` | `openclaw.query` | Query OpenClaw via the bridge. |
76
+ | `list_anomalies` | `anomalies` | `GET /api/v1/anomalies` | `anomalies.read` | Detected anomalies, with status/severity/source filtering. |
77
+ | `report_anomaly` | `report-anomaly` | `POST /api/v1/anomalies` | `anomalies.report` | Record an anomaly / self-repair signal. |
78
+
79
+ Tools without a CLI command are MCP-only. Every tool's permission is enforced by
80
+ the deployment: a key lacking a tool's permission gets a `403` the tool surfaces
81
+ as an error.
82
+
83
+ The four read-only diagnostic tools (`get_integrations`, `diagnose_chat`,
84
+ `get_chat_state`, `get_trace_enrichment`) plus the bounded corrective
85
+ `reconcile_chat` compose into a closed loop an agent can drive from a single user
86
+ report. See **[the self-correction loop](../docs/SELF_CORRECTION_LOOP.md)** for how
87
+ they fit together and the SOC2-safe trace catalog (including the `documentary.*`
88
+ document-fetch traces). The whole surface is metadata-only — never message text,
89
+ filenames, URLs, or keys.
90
+
91
+ ## CLI usage
92
+
93
+ ```bash
94
+ export OPENCLAW_WEBCHAT_API_BASE=http://127.0.0.1:3213
95
+ export OPENCLAW_WEBCHAT_API_KEY=oc_live_xxxxxxxxxxxx
96
+
97
+ node dist/cli.js health
98
+ node dist/cli.js compat
99
+ node dist/cli.js traces --limit 20
100
+ node dist/cli.js traces --kind api.call --correlation-id abc123
101
+
102
+ # filtering (traces): --q substring, time range, structured fields
103
+ # --from/--to accept epoch ms OR a Grafana relative token (now, now-<N><unit>)
104
+ node dist/cli.js traces --from now-24h --to now --status-class 4xx --kind api.call --q foo
105
+ node dist/cli.js traces --status 404 --principal-type service --role-key admin
106
+
107
+ node dist/cli.js kpi --metric api.calls --since 2026-06-01T00:00
108
+ node dist/cli.js kpi --metric api.calls --from now-24h --to now
109
+ node dist/cli.js anomalies --limit 50 --status open
110
+ node dist/cli.js anomalies --from now-1h --severity critical --source detector --q spike
111
+ node dist/cli.js query-openclaw --prompt "summarize last run" --chat-id c1
112
+ node dist/cli.js report-anomaly --kind latency.spike --severity warn --message "p99 > 5s"
113
+ ```
114
+
115
+ If installed/published, the same is available as the `atrium` bin
116
+ (e.g. `atrium traces --limit 20`).
117
+
118
+ ## OpenClaw MCP wiring
119
+
120
+ Register the stdio server in `~/.openclaw/openclaw.json`. Because this package
121
+ is not published, run the built file directly:
122
+
123
+ ```json
124
+ {
125
+ "mcpServers": {
126
+ "atrium": {
127
+ "command": "node",
128
+ "args": ["/absolute/path/to/atrium/mcp/dist/server.js"],
129
+ "env": {
130
+ "OPENCLAW_WEBCHAT_API_BASE": "http://127.0.0.1:3213",
131
+ "OPENCLAW_WEBCHAT_API_KEY": "oc_live_..."
132
+ }
133
+ }
134
+ }
135
+ }
136
+ ```
137
+
138
+ After publishing to a registry, the `npx` form works too:
139
+
140
+ ```json
141
+ {
142
+ "mcpServers": {
143
+ "atrium": {
144
+ "command": "npx",
145
+ "args": ["-y", "atrium-mcp"],
146
+ "env": {
147
+ "OPENCLAW_WEBCHAT_API_BASE": "https://<deployment>.convex.site",
148
+ "OPENCLAW_WEBCHAT_API_KEY": "oc_live_..."
149
+ }
150
+ }
151
+ }
152
+ }
153
+ ```
154
+
155
+ ## Security
156
+
157
+ - The API key lives in env only — never committed, never logged, never in a URL.
158
+ - Stdio transport avoids the DNS-rebinding surface of local HTTP MCP servers.
159
+ - Permission scoping is enforced by the deployment, not the client: a key that
160
+ lacks a tool's permission gets a `403` that the tool surfaces as an error.
package/dist/cli.d.ts ADDED
@@ -0,0 +1,23 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * atrium CLI — a thin shell client over the same /api/v1 calls the
4
+ * MCP server exposes. Same env-var auth (OPENCLAW_WEBCHAT_API_BASE +
5
+ * OPENCLAW_WEBCHAT_API_KEY); the Bearer key is only ever sent in the
6
+ * Authorization header (never echoed, never in a URL).
7
+ *
8
+ * Usage:
9
+ * atrium health
10
+ * atrium traces [--limit N] [--q TEXT] [--from T] [--to T] [--kind K]
11
+ * [--status CODE] [--status-class 2xx|4xx|5xx] [--direction D]
12
+ * [--principal-type T] [--role-key K] [--correlation-id ID]
13
+ * atrium kpi [--metric M] [--since ISO] [--from T] [--to T]
14
+ * atrium anomalies [--limit N] [--since ISO] [--q TEXT] [--from T]
15
+ * [--to T] [--status S] [--severity S] [--source S] [--kind K]
16
+ * atrium query-openclaw [--question TEXT]
17
+ * atrium report-anomaly --kind K --severity S --message M [--correlation-id ID]
18
+ * atrium bridge-status
19
+ * atrium sync --instance NAME
20
+ *
21
+ * --from/--to accept epoch ms OR a Grafana relative token (e.g. now-24h, now).
22
+ */
23
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,220 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * atrium CLI — a thin shell client over the same /api/v1 calls the
4
+ * MCP server exposes. Same env-var auth (OPENCLAW_WEBCHAT_API_BASE +
5
+ * OPENCLAW_WEBCHAT_API_KEY); the Bearer key is only ever sent in the
6
+ * Authorization header (never echoed, never in a URL).
7
+ *
8
+ * Usage:
9
+ * atrium health
10
+ * atrium traces [--limit N] [--q TEXT] [--from T] [--to T] [--kind K]
11
+ * [--status CODE] [--status-class 2xx|4xx|5xx] [--direction D]
12
+ * [--principal-type T] [--role-key K] [--correlation-id ID]
13
+ * atrium kpi [--metric M] [--since ISO] [--from T] [--to T]
14
+ * atrium anomalies [--limit N] [--since ISO] [--q TEXT] [--from T]
15
+ * [--to T] [--status S] [--severity S] [--source S] [--kind K]
16
+ * atrium query-openclaw [--question TEXT]
17
+ * atrium report-anomaly --kind K --severity S --message M [--correlation-id ID]
18
+ * atrium bridge-status
19
+ * atrium sync --instance NAME
20
+ *
21
+ * --from/--to accept epoch ms OR a Grafana relative token (e.g. now-24h, now).
22
+ */
23
+ import { ApiError, resolveConfig } from "./config.js";
24
+ import { bridgeStatus, getCompat, getKpi, getSchema, health, listAnomalies, listSchemas, listTraces, queryOpenClaw, reportAnomaly, syncInstance, } from "./tools.js";
25
+ /** Parse `--key value`, `--key=value`, and bare `--flag` arguments. */
26
+ function parseArgs(argv) {
27
+ const positionals = [];
28
+ const flags = {};
29
+ for (let i = 0; i < argv.length; i++) {
30
+ const token = argv[i];
31
+ if (token.startsWith("--")) {
32
+ const eq = token.indexOf("=");
33
+ if (eq !== -1) {
34
+ flags[token.slice(2, eq)] = token.slice(eq + 1);
35
+ }
36
+ else {
37
+ const next = argv[i + 1];
38
+ if (next !== undefined && !next.startsWith("--")) {
39
+ flags[token.slice(2)] = next;
40
+ i++;
41
+ }
42
+ else {
43
+ flags[token.slice(2)] = true;
44
+ }
45
+ }
46
+ }
47
+ else {
48
+ positionals.push(token);
49
+ }
50
+ }
51
+ return { positionals, flags };
52
+ }
53
+ function num(value) {
54
+ if (typeof value !== "string")
55
+ return undefined;
56
+ const n = Number(value);
57
+ return Number.isFinite(n) ? n : undefined;
58
+ }
59
+ function str(value) {
60
+ return typeof value === "string" ? value : undefined;
61
+ }
62
+ const USAGE = `atrium — thin CLI over the /api/v1 observability surface
63
+
64
+ Commands:
65
+ health GET /health
66
+ compat GET /compat (bridge.read)
67
+ bridge-status GET /bridge-status (bridge.read)
68
+ schemas GET /schemas (public)
69
+ schema --id NAME GET /schemas/:id (public)
70
+ sync --instance NAME POST /instances/sync (selfheal)
71
+ traces [--limit N] [--q TEXT] [--from T] [--to T] [--kind K] [--status CODE]
72
+ [--status-class 2xx|4xx|5xx] [--direction D] [--principal-type T]
73
+ [--role-key K] [--correlation-id ID]
74
+ GET /traces (traces.read)
75
+ kpi [--metric M] [--since ISO] [--from T] [--to T]
76
+ GET /kpi (kpi.read)
77
+ anomalies [--limit N] [--since ISO] [--q TEXT] [--from T] [--to T]
78
+ [--status S] [--severity S] [--source S] [--kind K]
79
+ GET /anomalies (anomalies.read)
80
+ query-openclaw [--question TEXT] POST /openclaw/query (openclaw.query)
81
+ report-anomaly --kind K --severity info|warn|critical --message M [--correlation-id ID]
82
+ POST /anomalies (anomalies.report)
83
+
84
+ Filters:
85
+ --from / --to accept epoch ms (e.g. 1717372800000) OR a Grafana relative
86
+ token: now, or now-<N><unit> with unit s|m|h|d|w (e.g. --from now-24h --to now).
87
+ Example: traces --from now-24h --to now --status-class 4xx --kind api.call --q foo
88
+
89
+ Environment:
90
+ OPENCLAW_WEBCHAT_API_BASE deployment .site origin (default http://127.0.0.1:3213)
91
+ OPENCLAW_WEBCHAT_API_KEY oc_live_ Bearer key (required)`;
92
+ async function dispatch(config, command, flags) {
93
+ switch (command) {
94
+ case "health":
95
+ return health(config);
96
+ case "compat":
97
+ return getCompat(config);
98
+ case "bridge-status":
99
+ return bridgeStatus(config);
100
+ case "schemas":
101
+ return listSchemas(config);
102
+ case "schema": {
103
+ const id = str(flags.id);
104
+ if (!id) {
105
+ throw new Error("schema requires --id NAME (e.g. --id provenance.v1)");
106
+ }
107
+ return getSchema(config, { id });
108
+ }
109
+ case "sync": {
110
+ const instance = str(flags.instance);
111
+ if (!instance) {
112
+ throw new Error("sync requires --instance NAME");
113
+ }
114
+ return syncInstance(config, { instance });
115
+ }
116
+ case "traces": {
117
+ const statusClass = str(flags["status-class"]);
118
+ if (statusClass !== undefined &&
119
+ statusClass !== "2xx" &&
120
+ statusClass !== "4xx" &&
121
+ statusClass !== "5xx") {
122
+ throw new Error("--status-class must be one of 2xx|4xx|5xx");
123
+ }
124
+ return listTraces(config, {
125
+ limit: num(flags.limit),
126
+ q: str(flags.q),
127
+ from: str(flags.from),
128
+ to: str(flags.to),
129
+ kind: str(flags.kind),
130
+ status: num(flags.status),
131
+ statusClass,
132
+ direction: str(flags.direction),
133
+ principalType: str(flags["principal-type"]),
134
+ roleKey: str(flags["role-key"]),
135
+ correlationId: str(flags["correlation-id"]),
136
+ });
137
+ }
138
+ case "kpi":
139
+ return getKpi(config, {
140
+ metric: str(flags.metric),
141
+ since: str(flags.since),
142
+ from: str(flags.from),
143
+ to: str(flags.to),
144
+ });
145
+ case "anomalies":
146
+ return listAnomalies(config, {
147
+ limit: num(flags.limit),
148
+ since: str(flags.since),
149
+ q: str(flags.q),
150
+ from: str(flags.from),
151
+ to: str(flags.to),
152
+ status: str(flags.status),
153
+ severity: str(flags.severity),
154
+ source: str(flags.source),
155
+ kind: str(flags.kind),
156
+ });
157
+ case "query-openclaw":
158
+ return queryOpenClaw(config, {
159
+ question: str(flags.question),
160
+ });
161
+ case "report-anomaly": {
162
+ const kind = str(flags.kind);
163
+ if (!kind) {
164
+ throw new Error("report-anomaly requires --kind");
165
+ }
166
+ const severity = str(flags.severity);
167
+ if (severity !== "info" && severity !== "warn" && severity !== "critical") {
168
+ throw new Error("report-anomaly requires --severity info|warn|critical");
169
+ }
170
+ const message = str(flags.message);
171
+ if (!message) {
172
+ throw new Error("report-anomaly requires --message");
173
+ }
174
+ return reportAnomaly(config, {
175
+ kind,
176
+ severity,
177
+ message,
178
+ correlationId: str(flags["correlation-id"]),
179
+ });
180
+ }
181
+ default:
182
+ throw new Error(`Unknown command: ${command}`);
183
+ }
184
+ }
185
+ async function main() {
186
+ const { positionals, flags } = parseArgs(process.argv.slice(2));
187
+ const command = positionals[0];
188
+ if (!command || command === "help" || flags.help) {
189
+ process.stdout.write(`${USAGE}\n`);
190
+ return;
191
+ }
192
+ let config;
193
+ try {
194
+ config = resolveConfig();
195
+ }
196
+ catch (err) {
197
+ // Config error (e.g. missing key). Message names the env var, not its value.
198
+ process.stderr.write(`${err.message}\n`);
199
+ process.exitCode = 2;
200
+ return;
201
+ }
202
+ try {
203
+ const result = await dispatch(config, command, flags);
204
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
205
+ }
206
+ catch (err) {
207
+ if (err instanceof ApiError) {
208
+ const bodyText = typeof err.body === "string"
209
+ ? err.body
210
+ : JSON.stringify(err.body, null, 2);
211
+ process.stderr.write(`API error ${err.status}: ${bodyText}\n`);
212
+ process.exitCode = 1;
213
+ return;
214
+ }
215
+ process.stderr.write(`${err.message}\n`);
216
+ process.exitCode = 1;
217
+ }
218
+ }
219
+ void main();
220
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Pure, testable HTTP client for the atrium /api/v1 surface.
3
+ *
4
+ * This module never imports anything from the Convex app — it only speaks HTTP.
5
+ * It reads two env vars:
6
+ * - OPENCLAW_WEBCHAT_API_BASE : the deployment `.site` origin, WITHOUT the
7
+ * `/api/v1` suffix (e.g. http://127.0.0.1:3213). The `/api/v1` prefix is
8
+ * added here so tool/CLI call-sites stay clean (`apiFetch(cfg, "/traces")`).
9
+ * - OPENCLAW_WEBCHAT_API_KEY : the `oc_live_...` Bearer key.
10
+ *
11
+ * The API key is only ever placed in the `Authorization` header — never in a
12
+ * URL/query string and never logged.
13
+ */
14
+ /** Default base origin for a local Convex deployment (.site origin). */
15
+ export declare const DEFAULT_API_BASE = "http://127.0.0.1:3213";
16
+ /** Path prefix for the observability API. Kept here so call-sites omit it. */
17
+ export declare const API_PREFIX = "/api/v1";
18
+ export interface Config {
19
+ /** Base origin (no trailing slash, no /api/v1). */
20
+ base: string;
21
+ /** The oc_live_ Bearer key. */
22
+ apiKey: string;
23
+ }
24
+ /** Minimal env shape so the resolver is injectable/testable. */
25
+ export type Env = Record<string, string | undefined>;
26
+ /**
27
+ * Structured error for any non-2xx response (or a fetch/transport failure).
28
+ * Carries the HTTP `status` and the parsed/raw `body` so callers can surface a
29
+ * clear message without re-reading the response. Never contains the API key.
30
+ */
31
+ export declare class ApiError extends Error {
32
+ readonly status: number;
33
+ readonly body: unknown;
34
+ constructor(status: number, body: unknown, message?: string);
35
+ }
36
+ /**
37
+ * Resolve config from an env bag (defaults to `process.env`). Throws a clear
38
+ * error naming the missing variable — without ever printing its value.
39
+ */
40
+ export declare function resolveConfig(env?: Env): Config;
41
+ /** Build the absolute URL for an API path (prepending `${base}/api/v1`). */
42
+ export declare function buildUrl(base: string, path: string): string;
43
+ /** Options for {@link apiFetch}. */
44
+ export interface ApiFetchOptions {
45
+ /** Injected fetch implementation. Defaults to the global `fetch`. */
46
+ fetchImpl?: typeof fetch;
47
+ }
48
+ /**
49
+ * Call the API at `path` (relative to `${base}/api/v1`), attaching the Bearer
50
+ * header, and parse the JSON response.
51
+ *
52
+ * - 2xx -> returns the parsed JSON (or `null` for an empty body).
53
+ * - non-2xx -> throws {@link ApiError} with `{status, body}` (body parsed as
54
+ * JSON when possible, otherwise the raw text).
55
+ * - transport failure -> throws {@link ApiError} with status 0.
56
+ *
57
+ * `fetch` is injectable for unit testing; no network call happens in tests.
58
+ */
59
+ export declare function apiFetch<T = unknown>(config: Config, path: string, init?: RequestInit, options?: ApiFetchOptions): Promise<T>;
package/dist/config.js ADDED
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Pure, testable HTTP client for the atrium /api/v1 surface.
3
+ *
4
+ * This module never imports anything from the Convex app — it only speaks HTTP.
5
+ * It reads two env vars:
6
+ * - OPENCLAW_WEBCHAT_API_BASE : the deployment `.site` origin, WITHOUT the
7
+ * `/api/v1` suffix (e.g. http://127.0.0.1:3213). The `/api/v1` prefix is
8
+ * added here so tool/CLI call-sites stay clean (`apiFetch(cfg, "/traces")`).
9
+ * - OPENCLAW_WEBCHAT_API_KEY : the `oc_live_...` Bearer key.
10
+ *
11
+ * The API key is only ever placed in the `Authorization` header — never in a
12
+ * URL/query string and never logged.
13
+ */
14
+ /** Default base origin for a local Convex deployment (.site origin). */
15
+ export const DEFAULT_API_BASE = "http://127.0.0.1:3213";
16
+ /** Path prefix for the observability API. Kept here so call-sites omit it. */
17
+ export const API_PREFIX = "/api/v1";
18
+ /**
19
+ * Structured error for any non-2xx response (or a fetch/transport failure).
20
+ * Carries the HTTP `status` and the parsed/raw `body` so callers can surface a
21
+ * clear message without re-reading the response. Never contains the API key.
22
+ */
23
+ export class ApiError extends Error {
24
+ status;
25
+ body;
26
+ constructor(status, body, message) {
27
+ super(message ?? `API request failed with status ${status}`);
28
+ this.name = "ApiError";
29
+ this.status = status;
30
+ this.body = body;
31
+ }
32
+ }
33
+ /**
34
+ * Resolve config from an env bag (defaults to `process.env`). Throws a clear
35
+ * error naming the missing variable — without ever printing its value.
36
+ */
37
+ export function resolveConfig(env = process.env) {
38
+ const base = (env.OPENCLAW_WEBCHAT_API_BASE ?? DEFAULT_API_BASE).replace(/\/+$/, "");
39
+ const apiKey = env.OPENCLAW_WEBCHAT_API_KEY;
40
+ if (!apiKey) {
41
+ throw new Error("OPENCLAW_WEBCHAT_API_KEY is required (set it in the environment).");
42
+ }
43
+ return { base, apiKey };
44
+ }
45
+ /** Build the absolute URL for an API path (prepending `${base}/api/v1`). */
46
+ export function buildUrl(base, path) {
47
+ const normalizedBase = base.replace(/\/+$/, "");
48
+ const normalizedPath = path.startsWith("/") ? path : `/${path}`;
49
+ return `${normalizedBase}${API_PREFIX}${normalizedPath}`;
50
+ }
51
+ /**
52
+ * Call the API at `path` (relative to `${base}/api/v1`), attaching the Bearer
53
+ * header, and parse the JSON response.
54
+ *
55
+ * - 2xx -> returns the parsed JSON (or `null` for an empty body).
56
+ * - non-2xx -> throws {@link ApiError} with `{status, body}` (body parsed as
57
+ * JSON when possible, otherwise the raw text).
58
+ * - transport failure -> throws {@link ApiError} with status 0.
59
+ *
60
+ * `fetch` is injectable for unit testing; no network call happens in tests.
61
+ */
62
+ export async function apiFetch(config, path, init = {}, options = {}) {
63
+ const doFetch = options.fetchImpl ?? globalThis.fetch;
64
+ if (typeof doFetch !== "function") {
65
+ throw new ApiError(0, null, "No fetch implementation available (Node >=18 or inject fetchImpl).");
66
+ }
67
+ const url = buildUrl(config.base, path);
68
+ const headers = new Headers(init.headers);
69
+ headers.set("Authorization", `Bearer ${config.apiKey}`);
70
+ headers.set("Accept", "application/json");
71
+ if (init.body !== undefined && !headers.has("Content-Type")) {
72
+ headers.set("Content-Type", "application/json");
73
+ }
74
+ let res;
75
+ try {
76
+ res = await doFetch(url, { ...init, headers });
77
+ }
78
+ catch (err) {
79
+ // Transport-level failure (DNS, connection refused, etc.). Surface it as a
80
+ // structured error with status 0 so callers handle it uniformly. Never
81
+ // include the request headers (which hold the key) in the message.
82
+ const message = err instanceof Error ? err.message : String(err);
83
+ throw new ApiError(0, null, `Network error reaching API: ${message}`);
84
+ }
85
+ const raw = await res.text();
86
+ const body = parseJsonSafe(raw);
87
+ if (!res.ok) {
88
+ throw new ApiError(res.status, body, `API ${res.status} ${res.statusText} for ${path}`);
89
+ }
90
+ return body;
91
+ }
92
+ /** Parse a string as JSON; fall back to the raw string when it isn't JSON. */
93
+ function parseJsonSafe(raw) {
94
+ if (raw.length === 0)
95
+ return null;
96
+ try {
97
+ return JSON.parse(raw);
98
+ }
99
+ catch {
100
+ return raw;
101
+ }
102
+ }
103
+ //# 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,15 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * atrium MCP server (stdio).
4
+ *
5
+ * A thin proxy over our /api/v1 observability surface. It carries an `oc_live_`
6
+ * Bearer key (from OPENCLAW_WEBCHAT_API_KEY) against the deployment `.site`
7
+ * origin (OPENCLAW_WEBCHAT_API_BASE) and exposes traces/KPIs/OpenClaw queries/
8
+ * anomalies as MCP tools for OpenClaw agents.
9
+ *
10
+ * It imports NOTHING from the Convex app — HTTP only. Each tool maps 1:1 to a
11
+ * permission enforced server-side (`requirePermission`), so a scoped key simply
12
+ * gets a 403 for tools it isn't allowed to call. A tool whose route is not yet
13
+ * deployed returns the API's response/error gracefully rather than crashing.
14
+ */
15
+ export {};