oops-errors-mcp 0.0.0-stage → 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/README.md +175 -2
- package/dist/audit.d.ts +42 -0
- package/dist/audit.js +53 -0
- package/dist/audit.js.map +1 -0
- package/dist/client.d.ts +68 -0
- package/dist/client.js +146 -0
- package/dist/client.js.map +1 -0
- package/dist/config.d.ts +9 -0
- package/dist/config.js +11 -0
- package/dist/config.js.map +1 -0
- package/dist/http.d.ts +2 -0
- package/dist/http.js +77 -0
- package/dist/http.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/issue.d.ts +17 -0
- package/dist/issue.js +42 -0
- package/dist/issue.js.map +1 -0
- package/dist/rbac.d.ts +47 -0
- package/dist/rbac.js +45 -0
- package/dist/rbac.js.map +1 -0
- package/dist/server.d.ts +42 -0
- package/dist/server.js +813 -0
- package/dist/server.js.map +1 -0
- package/dist/stdio.d.ts +2 -0
- package/dist/stdio.js +45 -0
- package/dist/stdio.js.map +1 -0
- package/dist/types.d.ts +294 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +2 -0
- package/dist/version.js.map +1 -0
- package/package.json +55 -4
package/README.md
CHANGED
|
@@ -1,3 +1,176 @@
|
|
|
1
|
-
#
|
|
1
|
+
# oops-errors-mcp
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[Model Context Protocol](https://modelcontextprotocol.io) server for
|
|
4
|
+
[oops](https://oops.3m4.net), the open-source error-tracking service. Lets an
|
|
5
|
+
AI agent (Claude Code, Claude Desktop, Cursor, the MCP Inspector, your own
|
|
6
|
+
app) list and triage issues, read stack traces and breadcrumbs, resolve or
|
|
7
|
+
ignore errors, check stats and manage projects — and report its *own*
|
|
8
|
+
failures into a project — with **role-based tool access**, a **human approval
|
|
9
|
+
step** for destructive actions, and an **audit trail**.
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
npx oops-errors-mcp # stdio (local hosts)
|
|
13
|
+
https://oops.3m4.net/mcp # Streamable HTTP (remote hosts)
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Quick start
|
|
17
|
+
|
|
18
|
+
1. Create an API key in the dashboard (`/dashboard/settings`) and pick a role.
|
|
19
|
+
2. Register the server with your host.
|
|
20
|
+
|
|
21
|
+
**Claude Code**
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
claude mcp add oops --scope user \
|
|
25
|
+
--env OOPS_API_KEY=oops_live_… \
|
|
26
|
+
-- npx -y oops-errors-mcp
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
**Claude Desktop / Cursor / any JSON config**
|
|
30
|
+
|
|
31
|
+
```json
|
|
32
|
+
{
|
|
33
|
+
"mcpServers": {
|
|
34
|
+
"oops": {
|
|
35
|
+
"command": "npx",
|
|
36
|
+
"args": ["-y", "oops-errors-mcp"],
|
|
37
|
+
"env": { "OOPS_API_KEY": "oops_live_…" }
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
**Remote (Streamable HTTP)** — no install, the key travels as a Bearer token:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
claude mcp add --transport http oops https://oops.3m4.net/mcp \
|
|
47
|
+
--header "Authorization: Bearer oops_live_…"
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Then ask: *"what is breaking in checkout today?"*, *"show me issue #12 and
|
|
51
|
+
its stack"*, *"which errors affect the most users since release 1.4.2?"*,
|
|
52
|
+
*"resolve every issue from the old release"*, *"triage the API project"*,
|
|
53
|
+
*"report this failure to the agents project"*.
|
|
54
|
+
|
|
55
|
+
## Configuration
|
|
56
|
+
|
|
57
|
+
| Variable | Default | Meaning |
|
|
58
|
+
|------------------|-------------------------------|---------|
|
|
59
|
+
| `OOPS_API_KEY` | — | Required for stdio. The oops management API key (`oops_live_…`). |
|
|
60
|
+
| `OOPS_API_URL` | `https://oops.3m4.net/api` | API base URL. Self-hosters point this at their instance. |
|
|
61
|
+
| `OOPS_AUDIT_LOG` | `stderr` | `stderr`, `none`, or a file path (JSON lines). |
|
|
62
|
+
| `OOPS_MCP_PORT` | `3333` | HTTP entrypoint only. |
|
|
63
|
+
|
|
64
|
+
Project public keys (`oops_pub_…`) are **not** accepted: they can only call
|
|
65
|
+
the ingest API and carry no role.
|
|
66
|
+
|
|
67
|
+
## Roles and tools (RBAC)
|
|
68
|
+
|
|
69
|
+
Each API key has a role. The server asks the API for the key's scopes once
|
|
70
|
+
(`GET /apikeys/me`) and **only registers the tools those scopes allow**, so a
|
|
71
|
+
read-only key never even lists `purge_events`. Every call is re-checked at
|
|
72
|
+
call time, and the API enforces the same scopes independently — three layers,
|
|
73
|
+
and a bug in the server can hide access but never grant it.
|
|
74
|
+
|
|
75
|
+
| Tool | Scope | viewer | editor | admin | Notes |
|
|
76
|
+
|----------------------|-------------------|:------:|:------:|:-----:|-------|
|
|
77
|
+
| `list_projects` | `projects:read` | ✓ | ✓ | ✓ | open issues, events 24h / 30d, users 30d, last event; `q` |
|
|
78
|
+
| `get_project` | `projects:read` | ✓ | ✓ | ✓ | public key, time zone, allowed origins, switches |
|
|
79
|
+
| `list_issues` | `issues:read` | ✓ | ✓ | ✓ | `status`, `level`, `environment`, `release`, `q`, `period`, `sort`, paging |
|
|
80
|
+
| `get_issue` | `issues:read` | ✓ | ✓ | ✓ | uuid, `12` or `#12`; includes the latest event with its stack |
|
|
81
|
+
| `list_issue_events` | `issues:read` | ✓ | ✓ | ✓ | occurrences of one issue, newest first |
|
|
82
|
+
| `get_event` | `issues:read` | ✓ | ✓ | ✓ | frames, raw stack, tags, extra, breadcrumbs, user |
|
|
83
|
+
| `search_events` | `issues:read` | ✓ | ✓ | ✓ | raw events; `level`, `environment`, `release`, `q`, `issue`, `period` |
|
|
84
|
+
| `get_stats` | `issues:read` | ✓ | ✓ | ✓ | totals vs previous period, series, breakdowns; `period` or `from`/`to` |
|
|
85
|
+
| `get_live` | `issues:read` | ✓ | ✓ | ✓ | events in the last few minutes |
|
|
86
|
+
| `get_usage` | `usage:read` | ✓ | ✓ | ✓ | events / dropped / counts vs. plan limits |
|
|
87
|
+
| `report_error` | `projects:read` | ✓ | ✓ | ✓ | posts an event to `/ingest` with the project's public key |
|
|
88
|
+
| `create_project` | `projects:write` | | ✓ | ✓ | returns the public key + SDK tag |
|
|
89
|
+
| `update_project` | `projects:write` | | ✓ | ✓ | pause ingest, store raw IPs, time zone, allowed origins… |
|
|
90
|
+
| `rotate_public_key` | `projects:write` | | ✓ | ✓ | old key stops working immediately |
|
|
91
|
+
| `update_issue` | `issues:write` | | ✓ | ✓ | `status`: `resolved`, `ignored`, `open` (reopen) |
|
|
92
|
+
| `bulk_update_issues` | `issues:write` | | ✓ | ✓ | same status on ≤ 100 issues |
|
|
93
|
+
| `delete_project` | `projects:delete` | | | ✓ | **confirmation required** |
|
|
94
|
+
| `purge_events` | `events:delete` | | | ✓ | **confirmation required**; deletes every event and issue |
|
|
95
|
+
| `get_audit_log` | `audit:read` | | | ✓ | `action`, `actor_type`, `outcome`, `since`, paging |
|
|
96
|
+
|
|
97
|
+
Roles are defined once in
|
|
98
|
+
[`packages/shared/api_key_roles.json`](../shared/api_key_roles.json) and
|
|
99
|
+
shared by the API, the dashboard and this server.
|
|
100
|
+
|
|
101
|
+
Also exposed: resources `oops://projects` and `oops://projects/{id}/summary`,
|
|
102
|
+
and the **`triage`** prompt (`project`, `period?`): summarise the open issues
|
|
103
|
+
of a project — new vs regressed, most users affected, by environment and
|
|
104
|
+
release — and propose what to fix, resolve or ignore first. It only proposes;
|
|
105
|
+
status changes still go through `update_issue` after the user agrees.
|
|
106
|
+
|
|
107
|
+
### Reporting from an agent
|
|
108
|
+
|
|
109
|
+
`report_error` lets the agent file its own failures: it reads the project to
|
|
110
|
+
get its public key and posts one event to `POST /api/ingest` with
|
|
111
|
+
`X-Project-Key`, `platform: "agent"` (override with `python`, `node`, …) and
|
|
112
|
+
`sdk: "oops-errors-mcp/<version>"`. Give `type` and/or `message`, optionally
|
|
113
|
+
`stack` (parsed server-side), `frames`, `url`, `release`, `environment`,
|
|
114
|
+
`user`, `tags`, `extra`, `breadcrumbs` and a `fingerprint` to control
|
|
115
|
+
grouping. The result is the event id and the issue `#number`, or `ok: false`
|
|
116
|
+
with a drop reason (`paused`, `origin`, `limit`) — never an error on the
|
|
117
|
+
agent's side.
|
|
118
|
+
|
|
119
|
+
## Human approval for destructive actions
|
|
120
|
+
|
|
121
|
+
`delete_project` and `purge_events` are irreversible, so they never run on the
|
|
122
|
+
model's say-so alone:
|
|
123
|
+
|
|
124
|
+
- On hosts that support MCP **elicitation** (Claude Code, Inspector) the user
|
|
125
|
+
gets a native confirmation dialog showing exactly what will be deleted.
|
|
126
|
+
- On hosts that don't (and always over HTTP, which is stateless), the tool
|
|
127
|
+
refuses with `confirmation_required` until the model calls it again with
|
|
128
|
+
`confirm: true` — which it can only honestly do after asking the user.
|
|
129
|
+
|
|
130
|
+
`rotate_public_key` is not gated but is marked destructive: every SDK tag and
|
|
131
|
+
server-side reporter using the previous key stops working the moment it runs.
|
|
132
|
+
Prefer `update_project` with `ingest_enabled: false` over deleting a project.
|
|
133
|
+
|
|
134
|
+
## Audit trail
|
|
135
|
+
|
|
136
|
+
Two complementary logs:
|
|
137
|
+
|
|
138
|
+
- **Server-side** (`audit_events` table, `GET /account/audit`, tool
|
|
139
|
+
`get_audit_log`): every mutation and every authorization denial, written in
|
|
140
|
+
the same transaction as the change.
|
|
141
|
+
- **Agent-side** (`OOPS_AUDIT_LOG`, JSON lines): every tool call the agent
|
|
142
|
+
attempted, including ones refused locally that never reached the API, with
|
|
143
|
+
duration, outcome (`ok|denied|error|cancelled|pending`) and compacted args.
|
|
144
|
+
|
|
145
|
+
## Design notes
|
|
146
|
+
|
|
147
|
+
- **Structured outputs.** Every tool declares an `outputSchema` and returns
|
|
148
|
+
`structuredContent`; hosts can validate and chain results without parsing
|
|
149
|
+
prose. The text part is a compact summary (one line per issue or event, the
|
|
150
|
+
stack of the latest event top-first). Objects mirroring API responses allow
|
|
151
|
+
extra fields, so an additive API change never breaks client-side validation.
|
|
152
|
+
- **Errors are results.** API errors come back as `isError` with the stable
|
|
153
|
+
code (`limit_reached` + `which`, `key_taken`, `invalid_timezone`,
|
|
154
|
+
`invalid_origins`, `insufficient_scope`, `not_found`, `invalid_issue_ref`…)
|
|
155
|
+
so the model can recover instead of crashing.
|
|
156
|
+
- **stdio hygiene.** stdout is the protocol channel; all logging goes to stderr.
|
|
157
|
+
- **Stateless HTTP.** Each request builds a server bound to the caller's key;
|
|
158
|
+
tenants can never share state. The `/apikeys/me` lookup is cached 60 s per key.
|
|
159
|
+
|
|
160
|
+
## Development
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
bun install
|
|
164
|
+
bun test # in-memory MCP client ↔ server against a fake API
|
|
165
|
+
bun run build
|
|
166
|
+
OOPS_API_KEY=… OOPS_API_URL=http://localhost:8084/api bun run inspect # MCP Inspector
|
|
167
|
+
OOPS_API_KEY=… OOPS_API_URL=http://localhost:8084/api bun run dev:http # http://localhost:3333/mcp
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
The Docker image (`packages/mcp-server/Dockerfile`) runs the HTTP flavour and
|
|
171
|
+
is wired into `deploy/docker-compose.yml` as the `mcp` service, exposed by
|
|
172
|
+
Caddy at `/mcp`.
|
|
173
|
+
|
|
174
|
+
## License
|
|
175
|
+
|
|
176
|
+
MIT © Enrique Magallon Alvarez
|
package/dist/audit.d.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export type AuditOutcome = "ok" | "denied" | "error" | "cancelled" | "pending";
|
|
2
|
+
export interface AuditEntry {
|
|
3
|
+
ts: string;
|
|
4
|
+
event: "server.start" | "tool.call";
|
|
5
|
+
transport: string;
|
|
6
|
+
actor: {
|
|
7
|
+
account: string;
|
|
8
|
+
prefix: string;
|
|
9
|
+
role: string;
|
|
10
|
+
};
|
|
11
|
+
tool?: string;
|
|
12
|
+
outcome?: AuditOutcome;
|
|
13
|
+
duration_ms?: number;
|
|
14
|
+
args?: Record<string, unknown>;
|
|
15
|
+
error?: {
|
|
16
|
+
code: string;
|
|
17
|
+
message: string;
|
|
18
|
+
};
|
|
19
|
+
meta?: Record<string, unknown>;
|
|
20
|
+
}
|
|
21
|
+
export interface AuditSink {
|
|
22
|
+
write(entry: AuditEntry): void;
|
|
23
|
+
}
|
|
24
|
+
export declare class StderrAuditSink implements AuditSink {
|
|
25
|
+
write(entry: AuditEntry): void;
|
|
26
|
+
}
|
|
27
|
+
export declare class FileAuditSink implements AuditSink {
|
|
28
|
+
private readonly path;
|
|
29
|
+
constructor(path: string);
|
|
30
|
+
write(entry: AuditEntry): void;
|
|
31
|
+
}
|
|
32
|
+
export declare class MemoryAuditSink implements AuditSink {
|
|
33
|
+
readonly entries: AuditEntry[];
|
|
34
|
+
write(entry: AuditEntry): void;
|
|
35
|
+
}
|
|
36
|
+
export declare class NullAuditSink implements AuditSink {
|
|
37
|
+
write(): void;
|
|
38
|
+
}
|
|
39
|
+
/** `OOPS_AUDIT_LOG`: "stderr" (default) | "none" | a file path. */
|
|
40
|
+
export declare function createAuditSink(setting: string | undefined): AuditSink;
|
|
41
|
+
/** Keep logged arguments small and free of giant payloads. */
|
|
42
|
+
export declare function compactArgs(args: unknown): Record<string, unknown> | undefined;
|
package/dist/audit.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client-side audit trail for the MCP server: one JSON line per tool call.
|
|
3
|
+
*
|
|
4
|
+
* This complements (does not replace) the server-side `audit_events` table:
|
|
5
|
+
* the API records *what changed*; this log records *what the agent tried*,
|
|
6
|
+
* including calls that were refused locally and never reached the API.
|
|
7
|
+
*
|
|
8
|
+
* In stdio mode stdout is the protocol channel, so the default sink is stderr.
|
|
9
|
+
*/
|
|
10
|
+
import { appendFileSync } from "node:fs";
|
|
11
|
+
export class StderrAuditSink {
|
|
12
|
+
write(entry) {
|
|
13
|
+
process.stderr.write(JSON.stringify(entry) + "\n");
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export class FileAuditSink {
|
|
17
|
+
path;
|
|
18
|
+
constructor(path) {
|
|
19
|
+
this.path = path;
|
|
20
|
+
}
|
|
21
|
+
write(entry) {
|
|
22
|
+
appendFileSync(this.path, JSON.stringify(entry) + "\n");
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export class MemoryAuditSink {
|
|
26
|
+
entries = [];
|
|
27
|
+
write(entry) {
|
|
28
|
+
this.entries.push(entry);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export class NullAuditSink {
|
|
32
|
+
write() { }
|
|
33
|
+
}
|
|
34
|
+
/** `OOPS_AUDIT_LOG`: "stderr" (default) | "none" | a file path. */
|
|
35
|
+
export function createAuditSink(setting) {
|
|
36
|
+
const value = (setting ?? "stderr").trim();
|
|
37
|
+
if (value === "" || value === "stderr")
|
|
38
|
+
return new StderrAuditSink();
|
|
39
|
+
if (value === "none")
|
|
40
|
+
return new NullAuditSink();
|
|
41
|
+
return new FileAuditSink(value);
|
|
42
|
+
}
|
|
43
|
+
/** Keep logged arguments small and free of giant payloads. */
|
|
44
|
+
export function compactArgs(args) {
|
|
45
|
+
if (!args || typeof args !== "object")
|
|
46
|
+
return undefined;
|
|
47
|
+
const out = {};
|
|
48
|
+
for (const [k, v] of Object.entries(args)) {
|
|
49
|
+
out[k] = typeof v === "string" && v.length > 200 ? v.slice(0, 200) + "…" : v;
|
|
50
|
+
}
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=audit.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audit.js","sourceRoot":"","sources":["../src/audit.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAqBzC,MAAM,OAAO,eAAe;IAC1B,KAAK,CAAC,KAAiB;QACrB,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC,CAAC;IACrD,CAAC;CACF;AAED,MAAM,OAAO,aAAa;IACK;IAA7B,YAA6B,IAAY;QAAZ,SAAI,GAAJ,IAAI,CAAQ;IAAG,CAAC;IAC7C,KAAK,CAAC,KAAiB;QACrB,cAAc,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC,CAAC;IAC1D,CAAC;CACF;AAED,MAAM,OAAO,eAAe;IACjB,OAAO,GAAiB,EAAE,CAAC;IACpC,KAAK,CAAC,KAAiB;QACrB,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAC3B,CAAC;CACF;AAED,MAAM,OAAO,aAAa;IACxB,KAAK,KAAU,CAAC;CACjB;AAED,mEAAmE;AACnE,MAAM,UAAU,eAAe,CAAC,OAA2B;IACzD,MAAM,KAAK,GAAG,CAAC,OAAO,IAAI,QAAQ,CAAC,CAAC,IAAI,EAAE,CAAC;IAC3C,IAAI,KAAK,KAAK,EAAE,IAAI,KAAK,KAAK,QAAQ;QAAE,OAAO,IAAI,eAAe,EAAE,CAAC;IACrE,IAAI,KAAK,KAAK,MAAM;QAAE,OAAO,IAAI,aAAa,EAAE,CAAC;IACjD,OAAO,IAAI,aAAa,CAAC,KAAK,CAAC,CAAC;AAClC,CAAC;AAED,8DAA8D;AAC9D,MAAM,UAAU,WAAW,CAAC,IAAa;IACvC,IAAI,CAAC,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ;QAAE,OAAO,SAAS,CAAC;IACxD,MAAM,GAAG,GAA4B,EAAE,CAAC;IACxC,KAAK,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,IAA+B,CAAC,EAAE,CAAC;QACrE,GAAG,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,CAAC,MAAM,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;IAC/E,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC"}
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal typed HTTP client for the oops management API (and the ingest
|
|
3
|
+
* endpoint, used by `report_error`).
|
|
4
|
+
*
|
|
5
|
+
* Deliberately dependency-free (global `fetch`) so the MCP server stays small
|
|
6
|
+
* and the client can be swapped for a fake in tests.
|
|
7
|
+
*/
|
|
8
|
+
import type { AuditEvent, AuditParams, CreateProjectInput, Event, EventsParams, IngestEventInput, IngestResult, Issue, IssueStatus, IssuesParams, KeyInfo, Live, Page, Project, PurgeResult, Stats, StatsParams, Summary, UpdateProjectInput, Usage } from "./types.js";
|
|
9
|
+
/** An error the API returned on purpose (`{ error: { code, message, ...extra } }`). */
|
|
10
|
+
export declare class OopsApiError extends Error {
|
|
11
|
+
readonly status: number;
|
|
12
|
+
readonly code: string;
|
|
13
|
+
/** Extra error fields, e.g. `which` on `limit_reached`, `required_scope` on `insufficient_scope`. */
|
|
14
|
+
readonly extra: Record<string, unknown>;
|
|
15
|
+
constructor(status: number, code: string, message: string,
|
|
16
|
+
/** Extra error fields, e.g. `which` on `limit_reached`, `required_scope` on `insufficient_scope`. */
|
|
17
|
+
extra?: Record<string, unknown>);
|
|
18
|
+
}
|
|
19
|
+
export interface OopsClientOptions {
|
|
20
|
+
/** Base URL including the `/api` prefix, e.g. `https://oops.3m4.net/api`. */
|
|
21
|
+
baseUrl: string;
|
|
22
|
+
apiKey: string;
|
|
23
|
+
fetch?: typeof fetch;
|
|
24
|
+
}
|
|
25
|
+
export declare class OopsClient {
|
|
26
|
+
private readonly baseUrl;
|
|
27
|
+
private readonly apiKey;
|
|
28
|
+
private readonly fetchImpl;
|
|
29
|
+
constructor(opts: OopsClientOptions);
|
|
30
|
+
private request;
|
|
31
|
+
/** Identity + scopes of the calling key. */
|
|
32
|
+
me(): Promise<KeyInfo>;
|
|
33
|
+
listProjects(): Promise<{
|
|
34
|
+
items: Project[];
|
|
35
|
+
}>;
|
|
36
|
+
createProject(input: CreateProjectInput): Promise<Project>;
|
|
37
|
+
getProject(id: string): Promise<Project>;
|
|
38
|
+
updateProject(id: string, patch: UpdateProjectInput): Promise<Project>;
|
|
39
|
+
deleteProject(id: string): Promise<void>;
|
|
40
|
+
/** New public key; the old one stops working at once. Returns the updated project. */
|
|
41
|
+
rotatePublicKey(id: string): Promise<Project>;
|
|
42
|
+
summary(projectId: string): Promise<Summary>;
|
|
43
|
+
/** Purge every event and issue of the project. */
|
|
44
|
+
purgeEvents(projectId: string): Promise<PurgeResult>;
|
|
45
|
+
listIssues(projectId: string, params?: IssuesParams): Promise<Page<Issue>>;
|
|
46
|
+
/** `issueRef` is the issue uuid or its per-project number as a string (see `parseIssueRef`). */
|
|
47
|
+
getIssue(projectId: string, issueRef: string): Promise<Issue>;
|
|
48
|
+
updateIssue(projectId: string, issueRef: string, status: IssueStatus): Promise<Issue>;
|
|
49
|
+
bulkUpdateIssues(projectId: string, ids: string[], status: IssueStatus): Promise<{
|
|
50
|
+
updated: number;
|
|
51
|
+
}>;
|
|
52
|
+
listIssueEvents(projectId: string, issueRef: string, params?: {
|
|
53
|
+
page?: number;
|
|
54
|
+
per_page?: number;
|
|
55
|
+
}): Promise<Page<Event>>;
|
|
56
|
+
listEvents(projectId: string, params?: EventsParams): Promise<Page<Event>>;
|
|
57
|
+
getEvent(projectId: string, eventId: string): Promise<Event>;
|
|
58
|
+
stats(projectId: string, params?: StatsParams): Promise<Stats>;
|
|
59
|
+
live(projectId: string): Promise<Live>;
|
|
60
|
+
usage(): Promise<Usage>;
|
|
61
|
+
auditLog(params?: AuditParams): Promise<Page<AuditEvent>>;
|
|
62
|
+
/**
|
|
63
|
+
* Report one event into a project. Authenticated with the project's PUBLIC
|
|
64
|
+
* key (`X-Project-Key`), never the API key: that is the only credential the
|
|
65
|
+
* ingest endpoint understands. Always 202; `ok: false` carries a drop reason.
|
|
66
|
+
*/
|
|
67
|
+
ingest(publicKey: string, event: IngestEventInput): Promise<IngestResult>;
|
|
68
|
+
}
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { VERSION } from "./version.js";
|
|
2
|
+
/** An error the API returned on purpose (`{ error: { code, message, ...extra } }`). */
|
|
3
|
+
export class OopsApiError extends Error {
|
|
4
|
+
status;
|
|
5
|
+
code;
|
|
6
|
+
extra;
|
|
7
|
+
constructor(status, code, message,
|
|
8
|
+
/** Extra error fields, e.g. `which` on `limit_reached`, `required_scope` on `insufficient_scope`. */
|
|
9
|
+
extra = {}) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.status = status;
|
|
12
|
+
this.code = code;
|
|
13
|
+
this.extra = extra;
|
|
14
|
+
this.name = "OopsApiError";
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
const enc = encodeURIComponent;
|
|
18
|
+
export class OopsClient {
|
|
19
|
+
baseUrl;
|
|
20
|
+
apiKey;
|
|
21
|
+
fetchImpl;
|
|
22
|
+
constructor(opts) {
|
|
23
|
+
this.baseUrl = opts.baseUrl.replace(/\/+$/, "");
|
|
24
|
+
this.apiKey = opts.apiKey;
|
|
25
|
+
this.fetchImpl = opts.fetch ?? globalThis.fetch;
|
|
26
|
+
}
|
|
27
|
+
async request(method, path, opts = {}) {
|
|
28
|
+
const url = new URL(this.baseUrl + path);
|
|
29
|
+
for (const [k, v] of Object.entries(opts.query ?? {})) {
|
|
30
|
+
if (v !== undefined && v !== "")
|
|
31
|
+
url.searchParams.set(k, String(v));
|
|
32
|
+
}
|
|
33
|
+
const headers = {
|
|
34
|
+
Accept: "application/json",
|
|
35
|
+
"User-Agent": `oops-errors-mcp/${VERSION}`,
|
|
36
|
+
...(opts.headers ?? {}),
|
|
37
|
+
};
|
|
38
|
+
if (opts.auth !== false)
|
|
39
|
+
headers["X-API-Key"] = this.apiKey;
|
|
40
|
+
if (opts.body !== undefined)
|
|
41
|
+
headers["Content-Type"] = "application/json";
|
|
42
|
+
const res = await this.fetchImpl(url, {
|
|
43
|
+
method,
|
|
44
|
+
headers,
|
|
45
|
+
body: opts.body === undefined ? undefined : JSON.stringify(opts.body),
|
|
46
|
+
});
|
|
47
|
+
if (res.status === 204)
|
|
48
|
+
return undefined;
|
|
49
|
+
const text = await res.text();
|
|
50
|
+
let data = null;
|
|
51
|
+
try {
|
|
52
|
+
data = text ? JSON.parse(text) : null;
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
data = null;
|
|
56
|
+
}
|
|
57
|
+
if (!res.ok) {
|
|
58
|
+
const err = (data?.error ?? {});
|
|
59
|
+
const { code, message, ...extra } = err;
|
|
60
|
+
throw new OopsApiError(res.status, typeof code === "string" ? code : `http_${res.status}`, typeof message === "string" ? message : `HTTP ${res.status}`, extra);
|
|
61
|
+
}
|
|
62
|
+
return data;
|
|
63
|
+
}
|
|
64
|
+
/** Identity + scopes of the calling key. */
|
|
65
|
+
me() {
|
|
66
|
+
return this.request("GET", "/apikeys/me");
|
|
67
|
+
}
|
|
68
|
+
// --- Projects ---------------------------------------------------------- //
|
|
69
|
+
listProjects() {
|
|
70
|
+
return this.request("GET", "/projects");
|
|
71
|
+
}
|
|
72
|
+
createProject(input) {
|
|
73
|
+
return this.request("POST", "/projects", { body: input });
|
|
74
|
+
}
|
|
75
|
+
getProject(id) {
|
|
76
|
+
return this.request("GET", `/projects/${enc(id)}`);
|
|
77
|
+
}
|
|
78
|
+
updateProject(id, patch) {
|
|
79
|
+
return this.request("PATCH", `/projects/${enc(id)}`, { body: patch });
|
|
80
|
+
}
|
|
81
|
+
deleteProject(id) {
|
|
82
|
+
return this.request("DELETE", `/projects/${enc(id)}`);
|
|
83
|
+
}
|
|
84
|
+
/** New public key; the old one stops working at once. Returns the updated project. */
|
|
85
|
+
rotatePublicKey(id) {
|
|
86
|
+
return this.request("POST", `/projects/${enc(id)}/rotate-key`);
|
|
87
|
+
}
|
|
88
|
+
summary(projectId) {
|
|
89
|
+
return this.request("GET", `/projects/${enc(projectId)}/summary`);
|
|
90
|
+
}
|
|
91
|
+
/** Purge every event and issue of the project. */
|
|
92
|
+
purgeEvents(projectId) {
|
|
93
|
+
return this.request("DELETE", `/projects/${enc(projectId)}/events`);
|
|
94
|
+
}
|
|
95
|
+
// --- Issues and events --------------------------------------------------- //
|
|
96
|
+
listIssues(projectId, params = {}) {
|
|
97
|
+
return this.request("GET", `/projects/${enc(projectId)}/issues`, { query: params });
|
|
98
|
+
}
|
|
99
|
+
/** `issueRef` is the issue uuid or its per-project number as a string (see `parseIssueRef`). */
|
|
100
|
+
getIssue(projectId, issueRef) {
|
|
101
|
+
return this.request("GET", `/projects/${enc(projectId)}/issues/${enc(issueRef)}`);
|
|
102
|
+
}
|
|
103
|
+
updateIssue(projectId, issueRef, status) {
|
|
104
|
+
return this.request("PATCH", `/projects/${enc(projectId)}/issues/${enc(issueRef)}`, { body: { status } });
|
|
105
|
+
}
|
|
106
|
+
bulkUpdateIssues(projectId, ids, status) {
|
|
107
|
+
return this.request("POST", `/projects/${enc(projectId)}/issues/bulk`, { body: { ids, status } });
|
|
108
|
+
}
|
|
109
|
+
listIssueEvents(projectId, issueRef, params = {}) {
|
|
110
|
+
return this.request("GET", `/projects/${enc(projectId)}/issues/${enc(issueRef)}/events`, { query: params });
|
|
111
|
+
}
|
|
112
|
+
listEvents(projectId, params = {}) {
|
|
113
|
+
return this.request("GET", `/projects/${enc(projectId)}/events`, { query: params });
|
|
114
|
+
}
|
|
115
|
+
getEvent(projectId, eventId) {
|
|
116
|
+
return this.request("GET", `/projects/${enc(projectId)}/events/${enc(eventId)}`);
|
|
117
|
+
}
|
|
118
|
+
// --- Stats ------------------------------------------------------------- //
|
|
119
|
+
stats(projectId, params = {}) {
|
|
120
|
+
return this.request("GET", `/projects/${enc(projectId)}/stats`, { query: params });
|
|
121
|
+
}
|
|
122
|
+
live(projectId) {
|
|
123
|
+
return this.request("GET", `/projects/${enc(projectId)}/live`);
|
|
124
|
+
}
|
|
125
|
+
// --- Account ----------------------------------------------------------- //
|
|
126
|
+
usage() {
|
|
127
|
+
return this.request("GET", "/account/usage");
|
|
128
|
+
}
|
|
129
|
+
auditLog(params = {}) {
|
|
130
|
+
return this.request("GET", "/account/audit", { query: params });
|
|
131
|
+
}
|
|
132
|
+
// --- Ingest (project public key, no API key) ---------------------------- //
|
|
133
|
+
/**
|
|
134
|
+
* Report one event into a project. Authenticated with the project's PUBLIC
|
|
135
|
+
* key (`X-Project-Key`), never the API key: that is the only credential the
|
|
136
|
+
* ingest endpoint understands. Always 202; `ok: false` carries a drop reason.
|
|
137
|
+
*/
|
|
138
|
+
ingest(publicKey, event) {
|
|
139
|
+
return this.request("POST", "/ingest", {
|
|
140
|
+
body: event,
|
|
141
|
+
auth: false,
|
|
142
|
+
headers: { "X-Project-Key": publicKey },
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Environment-driven configuration shared by the stdio and HTTP entrypoints. */
|
|
2
|
+
export interface Config {
|
|
3
|
+
apiUrl: string;
|
|
4
|
+
apiKey: string | undefined;
|
|
5
|
+
auditLog: string | undefined;
|
|
6
|
+
port: number;
|
|
7
|
+
}
|
|
8
|
+
export declare const DEFAULT_API_URL = "https://oops.3m4.net/api";
|
|
9
|
+
export declare function loadConfig(env?: NodeJS.ProcessEnv): Config;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Environment-driven configuration shared by the stdio and HTTP entrypoints. */
|
|
2
|
+
export const DEFAULT_API_URL = "https://oops.3m4.net/api";
|
|
3
|
+
export function loadConfig(env = process.env) {
|
|
4
|
+
return {
|
|
5
|
+
apiUrl: (env.OOPS_API_URL || DEFAULT_API_URL).replace(/\/+$/, ""),
|
|
6
|
+
apiKey: env.OOPS_API_KEY?.trim() || undefined,
|
|
7
|
+
auditLog: env.OOPS_AUDIT_LOG,
|
|
8
|
+
port: Number(env.OOPS_MCP_PORT || 3333),
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,iFAAiF;AASjF,MAAM,CAAC,MAAM,eAAe,GAAG,0BAA0B,CAAC;AAE1D,MAAM,UAAU,UAAU,CAAC,MAAyB,OAAO,CAAC,GAAG;IAC7D,OAAO;QACL,MAAM,EAAE,CAAC,GAAG,CAAC,YAAY,IAAI,eAAe,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC;QACjE,MAAM,EAAE,GAAG,CAAC,YAAY,EAAE,IAAI,EAAE,IAAI,SAAS;QAC7C,QAAQ,EAAE,GAAG,CAAC,cAAc;QAC5B,IAAI,EAAE,MAAM,CAAC,GAAG,CAAC,aAAa,IAAI,IAAI,CAAC;KACxC,CAAC;AACJ,CAAC"}
|
package/dist/http.d.ts
ADDED
package/dist/http.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Streamable HTTP entrypoint (remote MCP).
|
|
4
|
+
*
|
|
5
|
+
* - One endpoint, `POST /mcp`. Auth is the oops API key as a Bearer token
|
|
6
|
+
* (`Authorization: Bearer oops_live_…`); `X-API-Key` is accepted too.
|
|
7
|
+
* - Stateless: every request gets a fresh server + transport bound to the
|
|
8
|
+
* caller's key, so tenants can never share state. The cost is one
|
|
9
|
+
* `/apikeys/me` lookup per request, cached briefly per key.
|
|
10
|
+
* - No elicitation here: a stateless server has no back-channel to the user,
|
|
11
|
+
* so the destructive tools fall back to the explicit `confirm=true` flow.
|
|
12
|
+
*/
|
|
13
|
+
import { createHash } from "node:crypto";
|
|
14
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
15
|
+
import express from "express";
|
|
16
|
+
import { createAuditSink } from "./audit.js";
|
|
17
|
+
import { OopsApiError, OopsClient } from "./client.js";
|
|
18
|
+
import { loadConfig } from "./config.js";
|
|
19
|
+
import { createOopsServer } from "./server.js";
|
|
20
|
+
import { VERSION } from "./version.js";
|
|
21
|
+
const cfg = loadConfig();
|
|
22
|
+
const audit = createAuditSink(cfg.auditLog);
|
|
23
|
+
const ME_CACHE_MS = 60_000;
|
|
24
|
+
const meCache = new Map();
|
|
25
|
+
function extractKey(req) {
|
|
26
|
+
const auth = req.header("authorization");
|
|
27
|
+
if (auth?.toLowerCase().startsWith("bearer "))
|
|
28
|
+
return auth.slice(7).trim();
|
|
29
|
+
return req.header("x-api-key")?.trim() || undefined;
|
|
30
|
+
}
|
|
31
|
+
async function resolveKey(apiKey) {
|
|
32
|
+
const id = createHash("sha256").update(apiKey).digest("hex");
|
|
33
|
+
const hit = meCache.get(id);
|
|
34
|
+
if (hit && hit.expires > Date.now())
|
|
35
|
+
return hit.info;
|
|
36
|
+
const info = await new OopsClient({ baseUrl: cfg.apiUrl, apiKey }).me();
|
|
37
|
+
meCache.set(id, { info, expires: Date.now() + ME_CACHE_MS });
|
|
38
|
+
return info;
|
|
39
|
+
}
|
|
40
|
+
function rpcError(res, status, message) {
|
|
41
|
+
if (status === 401)
|
|
42
|
+
res.setHeader("WWW-Authenticate", 'Bearer realm="oops-errors-mcp"');
|
|
43
|
+
res.status(status).json({ jsonrpc: "2.0", error: { code: -32000, message }, id: null });
|
|
44
|
+
}
|
|
45
|
+
const app = express();
|
|
46
|
+
app.use(express.json({ limit: "1mb" }));
|
|
47
|
+
app.get("/healthz", (_req, res) => {
|
|
48
|
+
res.json({ status: "ok", name: "oops-errors-mcp", version: VERSION });
|
|
49
|
+
});
|
|
50
|
+
app.all("/mcp", async (req, res) => {
|
|
51
|
+
const apiKey = extractKey(req);
|
|
52
|
+
if (!apiKey)
|
|
53
|
+
return rpcError(res, 401, "Missing API key. Send `Authorization: Bearer oops_live_…`.");
|
|
54
|
+
let keyInfo;
|
|
55
|
+
try {
|
|
56
|
+
keyInfo = await resolveKey(apiKey);
|
|
57
|
+
}
|
|
58
|
+
catch (err) {
|
|
59
|
+
if (err instanceof OopsApiError && (err.status === 401 || err.status === 403)) {
|
|
60
|
+
return rpcError(res, err.status, err.message);
|
|
61
|
+
}
|
|
62
|
+
return rpcError(res, 502, `Upstream oops API unavailable: ${err instanceof Error ? err.message : String(err)}`);
|
|
63
|
+
}
|
|
64
|
+
const client = new OopsClient({ baseUrl: cfg.apiUrl, apiKey });
|
|
65
|
+
const server = createOopsServer({ client, keyInfo, audit, transport: "http", allowElicitation: false });
|
|
66
|
+
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
|
|
67
|
+
res.on("close", () => {
|
|
68
|
+
void transport.close();
|
|
69
|
+
void server.close();
|
|
70
|
+
});
|
|
71
|
+
await server.connect(transport);
|
|
72
|
+
await transport.handleRequest(req, res, req.body);
|
|
73
|
+
});
|
|
74
|
+
app.listen(cfg.port, "0.0.0.0", () => {
|
|
75
|
+
process.stderr.write(`oops-errors-mcp http listening on :${cfg.port} (api: ${cfg.apiUrl})\n`);
|
|
76
|
+
});
|
|
77
|
+
//# sourceMappingURL=http.js.map
|
package/dist/http.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http.js","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { createOopsServer, buildInstructions, type OopsServerOptions } from "./server.js";
|
|
2
|
+
export { OopsClient, OopsApiError, type OopsClientOptions } from "./client.js";
|
|
3
|
+
export { TOOL_SCOPES, ALL_TOOLS, CONFIRMED_TOOLS, allowedTools, hasScope, hasAllScopes, type ToolName } from "./rbac.js";
|
|
4
|
+
export { parseIssueRef, formatIssueRef, InvalidIssueRef } from "./issue.js";
|
|
5
|
+
export { createAuditSink, StderrAuditSink, FileAuditSink, MemoryAuditSink, NullAuditSink, type AuditSink, type AuditEntry, type AuditOutcome, } from "./audit.js";
|
|
6
|
+
export { loadConfig, DEFAULT_API_URL, type Config } from "./config.js";
|
|
7
|
+
export type * from "./types.js";
|
|
8
|
+
export { VERSION } from "./version.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { createOopsServer, buildInstructions } from "./server.js";
|
|
2
|
+
export { OopsClient, OopsApiError } from "./client.js";
|
|
3
|
+
export { TOOL_SCOPES, ALL_TOOLS, CONFIRMED_TOOLS, allowedTools, hasScope, hasAllScopes } from "./rbac.js";
|
|
4
|
+
export { parseIssueRef, formatIssueRef, InvalidIssueRef } from "./issue.js";
|
|
5
|
+
export { createAuditSink, StderrAuditSink, FileAuditSink, MemoryAuditSink, NullAuditSink, } from "./audit.js";
|
|
6
|
+
export { loadConfig, DEFAULT_API_URL } from "./config.js";
|
|
7
|
+
export { VERSION } from "./version.js";
|
|
8
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,iBAAiB,EAA0B,MAAM,aAAa,CAAC;AAC1F,OAAO,EAAE,UAAU,EAAE,YAAY,EAA0B,MAAM,aAAa,CAAC;AAC/E,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,eAAe,EAAE,YAAY,EAAE,QAAQ,EAAE,YAAY,EAAiB,MAAM,WAAW,CAAC;AACzH,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAC5E,OAAO,EACL,eAAe,EACf,eAAe,EACf,aAAa,EACb,eAAe,EACf,aAAa,GAId,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,UAAU,EAAE,eAAe,EAAe,MAAM,aAAa,CAAC;AAEvE,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC"}
|