@kaminari-ad/mcp 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/CHANGELOG.md ADDED
@@ -0,0 +1,383 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@kaminari-ad/mcp` are documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.1.0] - 2026-05-17
11
+
12
+ First public release of `@kaminari-ad/mcp`.
13
+
14
+ Bundles the full pre-release development arc that lived on a private
15
+ GitLab instance: HTTP + stdio transports, 82 MCP tools spanning scans /
16
+ campaigns / policies / alerts / webhooks / billing / accounts, tenant-
17
+ isolation suite, schema-backed parsers, openapi-fetch gateway, the
18
+ `prod:smoke` CI job, and the OSS bootstrap (LICENSE, SECURITY,
19
+ CONTRIBUTING, CODE_OF_CONDUCT, issue / PR templates).
20
+
21
+ See the GitHub release notes for the auto-generated commit summary;
22
+ the per-area changelog entries below capture the substantive engineering
23
+ work since the project was scaffolded.
24
+
25
+ ### Fixed (parser-drift phase 5 — post-review hardening)
26
+
27
+ - **`list_policy_sets` regression introduced by phase 2b.** The
28
+ schema-strict parser required `entries`, but `GET /api/v1/policy-sets`
29
+ returns `PolicySetListItem` (a slim per-item shape WITHOUT entries —
30
+ entries are loaded on demand via `get_policy_set(id)`). Every real
31
+ list call after the phase 2b roll-out returned
32
+ `"malformed policy-sets: 0.entries: Required"`. Now:
33
+ - New port DTO `PolicySetListItemResponse` (no `entries`).
34
+ - New `PolicySetListItemSchema` in `parse-policy-set.ts` backed by
35
+ the generated `schemas.PolicySetListItem`.
36
+ - `list_policy_sets` tool now exposes the slim shape and its
37
+ description tells the agent to follow up with `get_policy_set`
38
+ when it needs the entries.
39
+ - HTTP gateway test stub + fake fixture updated to the slim shape.
40
+ - Verified against prod API via end-to-end smoke against a sandbox
41
+ org: parser now accepts the real response.
42
+ - **`set_alert_destination_version` was parsing a 204 No Content as
43
+ JSON.** Pre-existing on `main` but surfaced more loudly under strict
44
+ zod. Now uses `parseEmpty`; port returns `Result<null>`; tool emits
45
+ `{ updated: true }` like the other 204 mutators. HTTP gateway test
46
+ updated to a 204 stub (the previous 200-with-body fixture masked
47
+ the real contract).
48
+ - **`create_api_key` rejected `expires_at: null`.** Common JSON-client
49
+ convention is to send `null` explicitly for "no expiry". Input
50
+ schema now accepts both `null` and omit; the gateway sees them as
51
+ equivalent (no `expires_at` field on the request body).
52
+ - **Dist bundle ~770 KB → ~210 KB.** The `openapi-zod-client`
53
+ generator emits a Zodios endpoints catalogue + `axios` client at
54
+ the tail of `zod-schemas.ts`. We use only the `schemas` bag for
55
+ runtime validation; the MCP gateway is `openapi-fetch`-based, not
56
+ Zodios. `scripts/gen-api-types.ts` now post-processes the generated
57
+ output to strip the Zodios runtime (imports, `endpoints` array,
58
+ `api` instance, `createApiClient` helper). Axios + `form-data` (which
59
+ use CJS `require("util")`) no longer reach the bundle — this also
60
+ fixes a `"Dynamic require of util is not supported"` crash on
61
+ `node dist/bin.js` startup that the integration smoke test caught.
62
+ `check:bundle-size` reverted to the original 500 KB ceiling.
63
+ - **`{set_id}` path templates corrected to `{policy_set_id}`** in
64
+ `http-api-gateway.ts` to match the OpenAPI spec. Runtime URLs are
65
+ identical (openapi-fetch substitutes by key name), but the literal
66
+ now lines up with the spec for future-readers.
67
+ - **CONTRIBUTING.md "Tenant isolation" §9** added — the pinned 5-key
68
+ outbound header allowlist (authorization / content-type / accept /
69
+ user-agent / x-request-id) is now an explicit numbered rule, not
70
+ an implicit one referenced from inline comments. Stale §8/§11
71
+ references in `http-api-gateway.ts`, `pino-logger.ts`, and the
72
+ isolation test headers updated to point at the correct sections.
73
+ - **Empty-body JSDocs filled in.** `parse-empty.ts::parseEmpty` and
74
+ `parse-count-envelope.ts::parseIntField` now document the contract
75
+ (204 No Content vs single-int envelope) instead of shipping
76
+ placeholder `/** * */` stubs.
77
+ - **Tool-test success-payload assertions.** Several 204-mutator tool
78
+ tests (`set_alert_destination_version`, `set_campaign_alert_overrides`,
79
+ `request_policy_set_approval`, `update_user_role`) only asserted
80
+ `isOk()` and never checked the synthetic `{ updated: true }` /
81
+ `{ requested: true }` payload — a regression that silently changed
82
+ the success shape would have slipped through. Each now asserts
83
+ both the call body AND the success payload.
84
+ - **Per-DTO parser error coverage gaps closed.** `parseScanTag`,
85
+ `parseUsage`, `parseUsageSummary`, `parseBalanceTx`, `parseInvoice`,
86
+ `parseAlertDestination`, `parseBulkReplay` were happy-path only.
87
+ Added missing-required + wrong-type cases to each. Coverage stays
88
+ 100% lines + 100% statements + 98.48% branches.
89
+
90
+ ### Fixed (parser-drift phase 1)
91
+
92
+ Production smoke against a fresh test org found 8 of the 82 tools
93
+ broken on 4 distinct root-cause patterns. All fixed; the global
94
+ masking bug that hid the real failure mode is fixed too.
95
+
96
+ - **API returns 204 No Content, parser expected entity.** Four tools
97
+ hit this — `set_campaign_alert_overrides`, `request_policy_set_approval`,
98
+ `update_tag_definition`, `update_user_role` — with errors like
99
+ `malformed user` / `malformed tag detail` / `malformed policy-set`.
100
+ Parsers now use `parseEmpty`; port DTOs return `null`; the tool
101
+ output is `{ updated: true }` / `{ requested: true }` so JSON output
102
+ stays a plain object. Tool descriptions updated to point at the
103
+ follow-up GET when the caller needs the new state echoed.
104
+ - **Action endpoint returns `GroupActionResponse` summary, parser
105
+ expected the group entity.** `archive_campaign_group` and
106
+ `unarchive_campaign_group` both POST endpoints return
107
+ `{ group_id, affected_campaigns, cancelled_count, run_ids,
108
+ failures }` like `run_campaign_group` / `cancel_campaign_group`.
109
+ Parsers now use the existing `parseGroupAction`; port DTO is
110
+ `GroupActionResponse`.
111
+ - **Paginated envelope vs bare array (inverted from last week).**
112
+ `list_campaign_groups` — OpenAPI documents the response as a bare
113
+ `CampaignGroupResponse[]` but the parser was `parseCampaignGroupPage`
114
+ expecting `{items, total, page, limit}`. New
115
+ `parseCampaignGroupArray` accepts both shapes defensively (same
116
+ `unwrapItems` pattern used in `parsePolicySetList`). Tool DTO becomes
117
+ `readonly CampaignGroupResponse[]` and the bogus `page` / `limit`
118
+ query params (the endpoint only documents `archived?`) are dropped.
119
+ - **Missing required request body + ignored response.** `test_webhook`
120
+ sent `undefined` body, but the API requires `{event_type: string}`
121
+ (422 without). The rich `TestWebhookResponse` was dropped via
122
+ `parseEmpty`. The gateway now sends the body; new
123
+ `parseTestWebhookResponse` decodes
124
+ `{ success, response_status, elapsed_ms, error_code, response_body }`
125
+ so the agent can diagnose a receiver failure from one call.
126
+ - **`error-mapping.ts::detail()` only handled string `detail`.**
127
+ FastAPI 422 returns `detail: ValidationError[]`. Every 422 across
128
+ every tool degraded to opaque `"Upstream error"` — which masked the
129
+ real `test_webhook` failure mode. The detail extractor now walks
130
+ the array, formats each entry as `"<loc>: <msg>"`, joins with
131
+ `; `, surfaces field-level RCA in the tool error string.
132
+
133
+ ### Added (parser-drift phase 2a — infrastructure for the rest)
134
+
135
+ - **Generated zod runtime schemas** at `src/shared/api/zod-schemas.ts`.
136
+ `scripts/gen-api-types.ts` now emits both
137
+ `src/shared/api/openapi.ts` (types, via `openapi-typescript`) and
138
+ `src/shared/api/zod-schemas.ts` (runtime schemas + Zodios endpoint
139
+ catalogue, via `openapi-zod-client`) from the SAME live OpenAPI
140
+ document — so the two files cannot drift relative to each other,
141
+ and the existing CI drift-check on the committed copies covers
142
+ both. Source URL changed from `https://kaminari.ad/openapi.json`
143
+ (returns a Next.js 404) to `https://app.kaminari.ad/openapi.json`
144
+ (the actual API host).
145
+ - **`parseWithSchema` helper** at
146
+ `src/infrastructure/api/parsers/parse-with-schema.ts` — wraps
147
+ `schema.safeParse()` with typed `ApiError` failure mapping AND
148
+ strips explicit-`undefined` keys from the parsed object so the
149
+ output matches the port DTO's `exactOptionalPropertyTypes` style.
150
+ Foundation for Phase 2b (converting each hand-written
151
+ `parse-*.ts` to a one-liner backed by `schemas.X.pick({...})`).
152
+
153
+ ### Added (parser-drift phase 4 — production observability)
154
+
155
+ - **`scripts/prod-smoke.ts`** + **`npm run prod:smoke`** + a manual
156
+ `prod:smoke` GitLab CI job. Fires a read-only subset of MCP tools
157
+ at the hosted endpoint using a long-lived sandbox-org bearer
158
+ (`KAMINARI_AD_MCP_PROD_TOKEN` CI variable, Masked + Protected).
159
+ Catches drifts that escape compile-time gates — API shape changes,
160
+ feature-flag-gated routes flipping on/off, parser regressions.
161
+ Manual trigger by default; flip to a daily schedule once the
162
+ sandbox org + token are provisioned.
163
+
164
+ ### Added (parser-drift phase 2b — full conversion)
165
+
166
+ - **Every `parse-*.ts` rewritten as a `parseWithSchema(schemas.X.pick
167
+ ({...}).strip())` one-liner.** All 17 hand-written parsers now
168
+ delegate to zod schemas generated from the live OpenAPI spec. A
169
+ field rename / removal upstream surfaces as a `tsc` error on the
170
+ `.pick({…})` mask (drift fails at compile time, not at the first
171
+ production request). A wrong-shape runtime payload degrades to a
172
+ typed `upstream` MCP error with the zod issue chain — never to an
173
+ `undefined.x` crash.
174
+ - **Test fixtures hardened.** ~30 fixtures across
175
+ `tests/unit/infrastructure/api/**` migrated from the old loose
176
+ hand-parser stubs (`id: "u1"`, `created_at: "t"`,
177
+ `status: "done"`) to schema-valid values
178
+ (`id: "00000000-0000-0000-0000-…"`, ISO datetimes, enum members
179
+ from the OpenAPI source like `"completed"` / `"api"`). Tests now
180
+ assert the same contract the production API enforces.
181
+ - **Two exempt files**, both documented in
182
+ `scripts/check-no-handwritten-parsers.ts`:
183
+ - `parse-empty.ts` — `204 No Content`, no body to validate.
184
+ - `parse-count-envelope.ts::parseIntField(raw, "x")` — generic
185
+ one-field-int extractor used by ad-hoc envelopes like
186
+ `{queued_count}` / `{cancelled_count}` that have no dedicated DTO
187
+ in the spec.
188
+
189
+ ### Added (parser-drift phase 3 — typed HTTP client)
190
+
191
+ - **`src/infrastructure/api/http-api-gateway.ts` ported to
192
+ `openapi-fetch`.** Every endpoint path is now a literal type
193
+ constrained by `paths` from `src/shared/api/openapi.ts`; path /
194
+ query / body shapes are validated by the same generated types,
195
+ with the agent-facing `Pick<S[K], …>` projections in
196
+ `domain/ports/api-gateway.ts` narrowing the surface. Renaming or
197
+ removing an endpoint on the API side fails the gateway at
198
+ `tsc --noEmit` immediately — no runtime drift.
199
+ - **Tenant-isolation contract preserved.** Pinned 5-key outbound
200
+ header allowlist (authorization / content-type / accept /
201
+ user-agent / x-request-id) — same shape the existing
202
+ `tests/isolation/header-injection-e2e.test.ts` AST gate already
203
+ enforces. Per-request `Dispatcher` injection (used by tests with
204
+ `MockAgent`; production uses the global agent) is forwarded through
205
+ a thin `fetchImpl` wrapper that splices `input.headers` /
206
+ `input.body` from openapi-fetch's `Request` onto the undici init
207
+ bag — without this the auth header silently vanishes (because
208
+ undici.fetch ignores `Request` and reads only `init`).
209
+ - **Removed `buildQuery` and the manual `${enc(id)}` interpolation.**
210
+ Query / path params now go through openapi-fetch's typed
211
+ `params.query` / `params.path` — typed key names per endpoint,
212
+ enforced at compile time (`scan_id` for `/scans/{scan_id}`,
213
+ `endpoint_id` for `/webhooks/{endpoint_id}`, etc.). A key typo
214
+ fails `tsc` rather than producing a silently-wrong URL.
215
+
216
+ ### Added (parser-drift phase 4 — production observability)
217
+
218
+ - **`scripts/prod-smoke.ts`** + **`npm run prod:smoke`** + a manual
219
+ `prod:smoke` GitLab CI job (description retained from phase 2a).
220
+ - **`npm run check:no-handwritten-parsers`** + new
221
+ `check:no-handwritten-parsers` GitLab CI job in `arch_gates`.
222
+ Belt-and-suspenders gate: parses every `src/infrastructure/api/
223
+ parsers/*.ts` and fails if a parser does NOT import `{ schemas }
224
+ from "../../shared/api/zod-schemas"` (or one of the two documented
225
+ exemptions). A future contributor who adds a hand-rolled
226
+ `typeof raw === "object" && "field" in raw` parser hits this gate
227
+ immediately, with a pointer to `parse-org` / `parse-scan` as the
228
+ canonical schema-backed shape.
229
+
230
+ ### Changed (breaking — pre-release, no API consumers yet)
231
+
232
+ - **Env vars now carry the `KAMINARI_AD_` namespace prefix.** Generic
233
+ names (`API_BASE_URL`, `LOG_LEVEL`, `HTTP_PORT`, `SESSION_TTL_SEC`,
234
+ `RATE_LIMIT_RPM`, `TRANSPORT`) were trivially poisonable by any
235
+ other tool in the same shell or container that set the same name.
236
+ All env vars are now `KAMINARI_AD_*`:
237
+ - `API_BASE_URL` → `KAMINARI_AD_API_URL`
238
+ - `LOG_LEVEL` → `KAMINARI_AD_LOG_LEVEL`
239
+ - `HTTP_PORT` → `KAMINARI_AD_HTTP_PORT`
240
+ - `SESSION_TTL_SEC` → `KAMINARI_AD_SESSION_TTL_SEC`
241
+ - `RATE_LIMIT_RPM` → `KAMINARI_AD_RATE_LIMIT_RPM`
242
+ - `TRANSPORT` → `KAMINARI_AD_TRANSPORT`
243
+ - `KAMINARI_AD_API_KEY` — unchanged.
244
+ Old unprefixed names are no longer read. `env | grep KAMINARI_AD_`
245
+ now enumerates every config input.
246
+
247
+ ### Added
248
+
249
+ - **`KAMINARI_AD_LOG_FORMAT`** env var (`pretty` | `json`) — was
250
+ previously hard-coded per transport (stdio = pretty, http = json).
251
+ Now operator-controllable. Default remains transport-dependent.
252
+ MCP hosts that capture stderr (Cursor, Claude Desktop) should set
253
+ this to `json` for parseable structured logs.
254
+ - **`pino-pretty`** moved to runtime `dependencies` so the default
255
+ `stdio + pretty` path no longer crashes on `npx -y @kaminari-ad/mcp`
256
+ with "unable to determine transport target".
257
+ - **Pino sink hardening**: `pretty` format now uses a sync
258
+ `pino-pretty` write stream wired to `process.stderr` directly,
259
+ instead of the `transport` worker option. The worker silently
260
+ ignores any `destination` argument and defaults to stdout, which
261
+ would corrupt the MCP JSON-RPC channel in stdio mode.
262
+
263
+ ### Fixed
264
+
265
+ - **Paginated list parsers (custom-rules, policy-sets) accept the
266
+ FastAPI envelope.** Both `parseCustomRuleArray` and
267
+ `parsePolicySetList` expected a bare `T[]`, but the API returns the
268
+ standard `{ items, total, page, limit, pages }` envelope — every
269
+ call to `list_custom_rules` / `list_policy_sets` returned `Upstream
270
+ error: expected array of …`. Parsers now accept both shapes
271
+ (envelope is unwrapped to the inner array; pagination metadata is
272
+ discarded since the tool DTOs return `readonly T[]`). Bug
273
+ surfaced by a real-org smoke run against a fresh
274
+ `/api/v1/custom-rules` instance. Other list parsers
275
+ (`parseScanPage`, `parseCampaignPage`, `parseAlertPage`) were
276
+ already envelope-aware; this brings the remaining two in line.
277
+
278
+ - **HTTP transport: Streamable HTTP session continuity.** The
279
+ per-request handler used to spin up a fresh `McpServer` +
280
+ `StreamableHTTPServerTransport` on every POST, so the SDK state
281
+ built by `initialize` evaporated before `initialized` arrived and
282
+ every subsequent request returned 400 "Server not initialized".
283
+ The handler now caches the SDK transport keyed by session id and
284
+ swaps a per-request `ApiGateway` into a `ctxRef` indirection so the
285
+ bearer stays request-scoped (tenant-isolation rule #9 enforced via
286
+ the existing `SessionStore` bearer-hash equality check). Refactored
287
+ into three files (`http-request-handler.ts`,
288
+ `session-resolver.ts`, `mcp-session-factory.ts`) to stay under the
289
+ 200-effective-line cap.
290
+ - **HTTP transport: SIGTERM/SIGINT crash on startup.** Both
291
+ bootstraps used `import * as process from "node:process"`; the
292
+ namespace import does not expose `process.once` (it's on the
293
+ default export only), so the HTTP transport crashed immediately
294
+ with "process.once is not a function" — and no unit/isolation test
295
+ caught it because none of them spawned the real bootstrap.
296
+ Switched all three entrypoints (`bin.ts`, `stdio-bootstrap.ts`,
297
+ `http-bootstrap.ts`) to default `import process from "node:process"`
298
+ and added a CLI-smoke integration test that boots the built
299
+ `dist/bin.js` and probes `/healthz` + the full
300
+ `initialize → initialized → tools/list` session flow.
301
+ - **stdio transport: pretty logs polluted the JSON-RPC stdout channel.**
302
+ Pino's `transport: { target: "pino-pretty" }` option spawns a worker
303
+ that defaults to `process.stdout` and ignores the destination
304
+ argument we passed to `pino()`. In stdio mode (MCP host reads
305
+ JSON-RPC from stdout) this corrupted every response. The pretty
306
+ format now uses a synchronous `pino-pretty` write stream wired to
307
+ `process.stderr` directly.
308
+ - `pino-pretty` moved from `devDependencies` to `dependencies` so a
309
+ fresh `npx -y @kaminari-ad/mcp` install no longer crashes with
310
+ "unable to determine transport target".
311
+ - `zod` pinned to `3.25.76` (was `3.24.1`). MCP SDK 1.29 ships a
312
+ transitive `zod-to-json-schema` that imports `zod/v3`, an export
313
+ introduced in zod 3.25 — the old pin would crash any consumer on
314
+ first import with `ERR_PACKAGE_PATH_NOT_EXPORTED`.
315
+
316
+ ## [0.1.0] - TBD
317
+
318
+ Initial public release. The first version that ships to npm under
319
+ `@kaminari-ad/mcp`.
320
+
321
+ ### Added
322
+
323
+ - **MCP server** with two transports:
324
+ - **stdio** for local clients (Cursor, Claude Desktop, Cline, ...).
325
+ API key from `KAMINARI_AD_API_KEY` env var.
326
+ - **Streamable HTTP** for the hosted endpoint at
327
+ `https://mcp.kaminari.ad/mcp`. Per-request Bearer, per-request
328
+ `ApiGateway` closure, in-memory session store keyed by `SessionId`
329
+ (value: `sha256(bearer)`), leaky-bucket rate limit keyed by
330
+ `sha256(bearer)`.
331
+ - **82 tools** spanning most of the `/api/v1` surface — account,
332
+ scans, campaigns, runs, campaign-groups, tag-definitions,
333
+ custom-rules, policy-sets, alerts, alert-notifications, webhooks,
334
+ billing, invoicing, geos, emulators. Every tool carries the
335
+ Anthropic Software Directory annotations (`title`, `readOnlyHint`,
336
+ `destructiveHint`, `idempotentHint`, `openWorldHint`).
337
+ - **Tenant-isolation discipline** for the hosted HTTP mode, codified
338
+ as 16 rules in `CONTRIBUTING.md` and enforced by `tests/isolation/`
339
+ (bearer-swap, header-injection, env-fallback disabled, missing
340
+ auth, concurrent bearers, no-shared-state AST gate, token-not-in-
341
+ logs, error-path isolation).
342
+ - **Type-safety**: every port DTO is a `Pick<components["schemas"][X],
343
+ ...>` projection over the generated `src/shared/api/openapi.ts`. A
344
+ future API field rename surfaces as a compile error in the parser.
345
+ - **Quality gates** (all enforced by CI):
346
+ - TypeScript strict + 8 ESLint plugins.
347
+ - 4 custom architecture gates: file size (200 LOC), import
348
+ boundaries (dependency-cruiser with `tsPreCompilationDeps`),
349
+ no-shared-state (ts-morph AST walk), tool-naming.
350
+ - 100% lines / 100% functions / 100% statements coverage,
351
+ 97% branches.
352
+ - Bundle size cap (500 KB on `dist/bin.js`, currently ~180 KB).
353
+ - `npm audit` with 0 vulnerabilities.
354
+ - **Packaging**: ESM dist with shebang for `npx`, sourcemaps, type
355
+ declarations, npm provenance on release.
356
+
357
+ ### Security
358
+
359
+ - `KAMINARI_AD_API_KEY` env var is **rejected on startup** in HTTP
360
+ mode (stdio only) — no fallback token can exist on a hosted server.
361
+ - Bearer tokens are **never logged**. The `BearerToken` value object
362
+ overrides `toString` / `toJSON` /
363
+ `Symbol.for("nodejs.util.inspect.custom")` to return
364
+ `[BearerToken redacted]`.
365
+ - pino is configured with redaction paths covering
366
+ `authorization` / `bearer` / `*.token` etc.
367
+ - Outbound API calls carry an **explicit 5-key header allowlist**
368
+ (`authorization`, `content-type`, `accept`, `user-agent`,
369
+ `x-request-id`). No spread of inbound request headers.
370
+
371
+ ### Not yet (tracked as follow-ups)
372
+
373
+ - **OAuth 2.1 + Dynamic Client Registration + PKCE** on the hosted
374
+ `mcp.kaminari.ad` endpoint, required by the Anthropic Software
375
+ Directory (§5.D) for remote MCP servers. v0.1.0 ships with raw
376
+ Bearer; OAuth lands in a follow-up MR.
377
+ - Binary scan-screenshot fetchers (`/api/v1/scans/{id}/screenshot`
378
+ etc.) — agents rarely consume images directly. Reach out if you
379
+ need them.
380
+ - Invoice PDF fetcher — same reason.
381
+
382
+ [Unreleased]: https://github.com/kaminari-ad/mcp/compare/v0.1.0...HEAD
383
+ [0.1.0]: https://github.com/kaminari-ad/mcp/releases/tag/v0.1.0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Kaminari Ad
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,159 @@
1
+ # `@kaminari-ad/mcp`
2
+
3
+ Official Model Context Protocol (MCP) server for [Kaminari Ad](https://kaminari.ad) — the ad verification platform from the team behind [Kaminari.Click](https://kaminari.click).
4
+
5
+ Lets AI agents (Cursor, Claude Desktop, Cline, and any MCP-compatible client) launch scans, inspect results, manage campaigns and policies, and read alerts directly against your Kaminari Ad workspace via your API key.
6
+
7
+ [![npm](https://img.shields.io/npm/v/@kaminari-ad/mcp.svg)](https://www.npmjs.com/package/@kaminari-ad/mcp)
8
+ [![CI](https://github.com/kaminari-ad/mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/kaminari-ad/mcp/actions/workflows/ci.yml)
9
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
10
+ [![Provenance](https://img.shields.io/npm/v/@kaminari-ad/mcp?label=provenance&logo=github)](https://www.npmjs.com/package/@kaminari-ad/mcp)
11
+
12
+ ---
13
+
14
+ ## Quick start
15
+
16
+ ### 1. Sign up & get an API key
17
+
18
+ 1. Sign up at [https://app.kaminari.ad/signup](https://app.kaminari.ad/signup) (free tier, no card required).
19
+ 2. Once signed in, go to **Settings → API Keys** and generate a new key, OR have an existing AI assistant (with a temporary login) call the [`create_api_key`](#tools) tool — both paths produce the same result.
20
+ 3. The key is shown **once**. Copy it. The full key is hashed server-side immediately.
21
+
22
+ > Tip for evaluators / Anthropic Software Directory reviewers: ask the team at [hello@kaminari.ad](mailto:hello@kaminari.ad) for a sandboxed test account with seeded sample scans, campaigns, and alerts.
23
+
24
+ ### 2a. Local install (stdio transport)
25
+
26
+ Add to your MCP client config (Cursor: `~/.cursor/mcp.json`; Claude Desktop: `~/Library/Application Support/Claude/claude_desktop_config.json`):
27
+
28
+ ```jsonc
29
+ {
30
+ "mcpServers": {
31
+ "kaminari-ad": {
32
+ "command": "npx",
33
+ "args": ["-y", "@kaminari-ad/mcp"],
34
+ "env": {
35
+ "KAMINARI_AD_API_KEY": "kad_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
36
+ },
37
+ },
38
+ },
39
+ }
40
+ ```
41
+
42
+ Restart your client. You should see `kaminari-ad` in the MCP servers list with 82 tools exposed.
43
+
44
+ ### 2b. Hosted HTTP transport (no install)
45
+
46
+ For cloud agents or clients without a local Node runtime, point at the hosted endpoint:
47
+
48
+ ```jsonc
49
+ {
50
+ "mcpServers": {
51
+ "kaminari-ad": {
52
+ "url": "https://mcp.kaminari.ad/mcp",
53
+ "headers": {
54
+ "Authorization": "Bearer kad_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
55
+ },
56
+ },
57
+ },
58
+ }
59
+ ```
60
+
61
+ ---
62
+
63
+ ## Tools
64
+
65
+ 82 tools mirroring most of the public `/api/v1` surface of Kaminari Ad. Every tool carries MCP behaviour annotations (`title`, `readOnlyHint`, `destructiveHint`, `idempotentHint`, `openWorldHint`) so MCP clients can warn before destructive actions. Highlights:
66
+
67
+ - **Account** (11) — `get_account`, `update_org`, `list_org_users`, `invite_user`, `update_user_role`, `remove_user`, `transfer_ownership`, `list_org_roles`, `list_api_keys`, `create_api_key`, `revoke_api_key`
68
+ - **Scans** (7) — `list_scans`, `get_scan`, `create_scan`, `create_bulk_scans`, `recheck_scans`, `cancel_scan`, `list_scan_tags`
69
+ - **Campaigns** (9) — `list_campaigns`, `get_campaign`, `create_campaign`, `update_campaign`, `archive_campaign`, `unarchive_campaign`, `cancel_campaign`, `run_campaign`, `list_campaign_runs`
70
+ - **Campaign groups** (10) — list/get/create/update/run/cancel/archive/unarchive + `pause_campaign_group_schedule`, `resume_campaign_group_schedule`
71
+ - **Runs** (3) — `get_run`, `list_run_scans`, `cancel_run` (use `list_campaign_runs` to enumerate runs of a campaign — the API has no standalone `/runs` index)
72
+ - **Tags** (4) — `list_tags`, `get_tag_definition`, `update_tag_definition`, `delete_tag_definition`
73
+ - **Custom rules** (6) — `list_custom_rules`, `get_custom_rule`, `create_custom_rule`, `update_custom_rule`, `delete_custom_rule`, `test_custom_rule`
74
+ - **Policy sets** (6) — `list_policy_sets`, `get_policy_set`, `create_policy_set`, `update_policy_set`, `delete_policy_set`, `request_policy_set_approval`
75
+ - **Alerts** (3) — `list_alerts`, `update_alert_status`, `get_alert_stats`
76
+ - **Webhooks** (11) — `list_webhooks`, `get_webhook`, `create_webhook`, `update_webhook`, `delete_webhook`, `list_webhook_event_types`, `list_webhook_deliveries`, `test_webhook`, `rotate_webhook_secret`, `replay_webhook_delivery`, `bulk_replay_webhook`
77
+ - **Billing** (4) — `get_billing_summary`, `list_usage`, `get_usage_summary`, `list_balance_history`
78
+ - **Invoicing** (1) — `list_invoices`
79
+ - **Alert notifications** (5) — `list_alert_destinations`, `delete_alert_destination`, `set_alert_destination_version`, `get_campaign_alert_overrides`, `set_campaign_alert_overrides`
80
+ - **Reference data** (2) — `list_geos`, `list_emulators`
81
+
82
+ Not exposed (intentionally): binary scan-screenshot fetchers, invoice PDF, the UI-only campaign picker, and the public marketing forms (`/contact`, `/demo-inquiries`). Open an issue if you need one of those.
83
+
84
+ ## Example agent prompts
85
+
86
+ These three prompts each exercise a different cross-section of tools and demonstrate the typical agent workflow:
87
+
88
+ 1. **"Scan https://news.example.com/article-promo across US, UK, DE on mobile profiles, flag anything that redirects to a paywall."** Touches `list_emulators` → `create_bulk_scans` → wait → `list_scans` (status=completed) → `get_scan` → `list_scan_tags`.
89
+ 2. **"Create a campaign that re-checks the homepage of brand-x.com every hour from JP and US; alert me on Slack if it ever shows a malware tag."** Touches `list_emulators` → `list_policy_sets` (find one with `malware`) → `create_campaign` (schedule_enabled=true) → `list_alert_destinations` → `set_campaign_alert_overrides`.
90
+ 3. **"What did I spend on ad verification last month, and which campaigns drove the cost?"** Touches `get_usage_summary` → `list_usage` (with date_from/date_to) → group by `scan_id` → `get_scan` → `get_campaign` for attribution.
91
+
92
+ Full machine-readable tool listing is exposed by the server itself — connect with any MCP client and call `tools/list`.
93
+
94
+ ---
95
+
96
+ ## Security & tenant isolation
97
+
98
+ The hosted HTTP endpoint serves many organizations from a single process. We take cross-tenant isolation very seriously:
99
+
100
+ - The MCP server is a strict, stateless, per-request pass-through. It forwards your `Authorization` header to the Kaminari Ad API verbatim and stores no per-tenant state between requests.
101
+ - No caches, no in-memory data indexed by anything tenant-related.
102
+ - `KAMINARI_AD_API_KEY` env var is rejected on startup in HTTP mode (stdio only) — no default fallback token exists.
103
+ - Session IDs are bound to the SHA-256 of the Bearer that initialized them; reuse with a different Bearer is rejected.
104
+ - Bearers are never logged. Only their 8-character hash prefix is recorded for correlation.
105
+ - See [`tests/isolation/`](tests/isolation) for the regression suite that enforces every rule above on each CI run.
106
+
107
+ To report a security issue, see [SECURITY.md](SECURITY.md).
108
+
109
+ ---
110
+
111
+ ## Development
112
+
113
+ The Docker path (no local Node required for the build, but see [CONTRIBUTING](CONTRIBUTING.md) for the host-side commit hooks):
114
+
115
+ ```bash
116
+ make check # lint + format-check + typecheck + arch-gates + test-cov
117
+ make test # full test suite
118
+ make test-unit # unit only
119
+ make test-isolation # tenant-isolation suite
120
+ ```
121
+
122
+ Or directly with `npm` if you have Node 22 LTS on the host (matches `.nvmrc` / `engines.node`):
123
+
124
+ ```bash
125
+ npm ci --legacy-peer-deps
126
+ npm run lint && npm run typecheck && npm test
127
+ ```
128
+
129
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for the development workflow and how to add a tool.
130
+
131
+ > The maintainers run the full development gate (integration tests, deploy automation, prod smoke) on a private GitLab instance and mirror the repo to GitHub. The public CI on GitHub Actions ([`.github/workflows/ci.yml`](.github/workflows/ci.yml)) runs lint + typecheck + unit tests + build + bundle-size check on every community PR, so contributors get fast green/red feedback without needing access to the internal infra. Tag pushes (`v*.*.*`) trigger [`.github/workflows/release.yml`](.github/workflows/release.yml), which publishes the package to npm with OIDC provenance and creates the GitHub Release.
132
+
133
+ ---
134
+
135
+ ## Stability
136
+
137
+ The **public surface** of this package is:
138
+
139
+ 1. The **CLI binary** `kaminari-ad-mcp` and its `--transport stdio|http` flag, the env vars documented in `.env.example`, and the exit codes (0 / 1 fatal / 2 invalid config).
140
+ 2. The **MCP wire protocol** as implemented by every registered tool (tool names, input schemas, output shapes, annotations). Tools deprecated in a future major version will keep working for at least one minor version with a console warning.
141
+
142
+ Everything else — the TypeScript types exported from `dist/bin.d.ts`, deep imports, internal class shapes — is **not** part of the public contract and may change in any release. Treat this package as a CLI, not a library.
143
+
144
+ We follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html) for the two items above. See [`CHANGELOG.md`](CHANGELOG.md) for the per-release record.
145
+
146
+ ---
147
+
148
+ ## Privacy
149
+
150
+ - **Data collected by the MCP server itself:** none beyond the `Authorization` header it forwards. In HTTP mode, only ephemeral per-request scoped state (session id ↔ Bearer hash, leaky-bucket rate limit by Bearer hash) is held in memory.
151
+ - **Data forwarded to Kaminari Ad:** every tool call is a thin pass-through to `/api/v1` over HTTPS. The Kaminari Ad privacy policy applies: [https://kaminari.ad/legal/privacy](https://kaminari.ad/legal/privacy).
152
+ - **Logs:** structured pino output, JSON in HTTP mode. The full Bearer token is redacted; only `bearer_hash = sha256(token).slice(0,8)` makes it into a log line, alongside `request_id`, `tool_name`, `api_status`, `elapsed_ms`. Tool inputs (which may contain customer scan IDs / URLs) are NOT logged.
153
+ - **Telemetry:** none. The OSS build ships a `NoopErrorReporter`. We do not bundle Sentry, OpenTelemetry exporters, or PostHog.
154
+
155
+ To report a security or privacy issue, see [SECURITY.md](SECURITY.md).
156
+
157
+ ## License
158
+
159
+ MIT — see [LICENSE](LICENSE).
package/SECURITY.md ADDED
@@ -0,0 +1,51 @@
1
+ # Security Policy
2
+
3
+ ## Reporting a Vulnerability
4
+
5
+ If you discover a security vulnerability in `@kaminari-ad/mcp` — particularly anything related to **cross-tenant data leakage** in the hosted HTTP endpoint — please report it privately.
6
+
7
+ **Do NOT** open a public GitHub or GitLab issue, and do not discuss it in chat channels.
8
+
9
+ ### How to report
10
+
11
+ Email **security@kaminari.ad** with:
12
+
13
+ - A clear description of the issue.
14
+ - Steps to reproduce (minimal repro preferred).
15
+ - Affected version(s) (`npm view @kaminari-ad/mcp version` and/or the `mcp.kaminari.ad` deployment time, if known).
16
+ - Your assessment of impact (data leak, auth bypass, DoS, etc.).
17
+ - Whether you would like to be credited in the security advisory.
18
+
19
+ You should receive an acknowledgement within 2 business days.
20
+
21
+ ### Disclosure timeline
22
+
23
+ We follow a coordinated 90-day disclosure window:
24
+
25
+ - **Day 0**: report received and acknowledged.
26
+ - **Day 0-14**: triage and reproduction; severity assessment.
27
+ - **Day 14-60**: fix developed, reviewed, tested, including a regression test in `tests/isolation/`.
28
+ - **Day 60-90**: patched release published to npm; deployed to `mcp.kaminari.ad`; coordinated public disclosure with the reporter.
29
+ - After **Day 90**, we will publish a security advisory regardless of whether a fix has shipped, so users can take their own mitigations.
30
+
31
+ If the issue is being actively exploited in the wild, the timeline is compressed and we coordinate the disclosure with the reporter directly.
32
+
33
+ ## Scope
34
+
35
+ Vulnerabilities of particular interest:
36
+
37
+ - **Tenant isolation breaches** in the HTTP transport: any way for one client's request to receive data from another client's session, log, cache, or in-flight request.
38
+ - **Token leakage**: any way a Bearer token can be reconstructed from logs, error responses, telemetry, or process memory exposed to operators.
39
+ - **Auth bypass**: any way to call a tool without a valid `Authorization` header or with a header that has been mutated by the MCP server.
40
+ - **Header injection**: any way to influence the outbound request to the Kaminari Ad API by injecting `X-*` headers, cookies, or other channels.
41
+ - **Supply-chain compromise**: any anomaly in the published npm package, the Docker image, or the GitLab/GitHub release pipeline.
42
+
43
+ Out of scope:
44
+
45
+ - Issues in the Kaminari Ad API itself (`/api/v1`). The API enforces its own authorization. Report API issues to the same `security@kaminari.ad` address, but note they live in the `api/` repository, not here.
46
+ - Issues in third-party MCP clients (Cursor, Claude Desktop, etc.).
47
+ - Theoretical issues with no demonstrable exploit path.
48
+
49
+ ## Recognition
50
+
51
+ With your permission, we will acknowledge your contribution in the [`CHANGELOG.md`](CHANGELOG.md) and in the GitHub security advisory.
package/dist/bin.d.ts ADDED
@@ -0,0 +1,2 @@
1
+
2
+ export { }