@kaminari-ad/mcp 0.2.2 → 0.4.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 CHANGED
@@ -7,6 +7,156 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ### Added
11
+
12
+ - **OAuth 2.0 Protected Resource discovery.** The HTTP transport now
13
+ serves `GET /.well-known/oauth-protected-resource` (RFC 9728) and
14
+ returns `WWW-Authenticate: Bearer resource_metadata="…", scope="…"`
15
+ on unauthenticated `/mcp` requests (the 401s produced by the
16
+ Bearer-extraction layer). The metadata document points at the
17
+ Kaminari Ad Authorization Server (`https://app.kaminari.ad` by
18
+ default) and advertises the `mcp:*` scope catalogue. This is the
19
+ discovery path Anthropic's Claude clients use when adding the
20
+ server as a directory connector. API-key authentication remains
21
+ fully supported — OAuth Bearer tokens are an additive, parallel
22
+ flavour for agents that prefer per-app consent + revocation.
23
+ - **Operator config:** `KAMINARI_AD_OAUTH_PROTECTED_RESOURCE`,
24
+ `KAMINARI_AD_OAUTH_PROTECTED_RESOURCE_METADATA_URL`,
25
+ `KAMINARI_AD_OAUTH_AUTHORIZATION_SERVER_URL`, and
26
+ `KAMINARI_AD_OAUTH_SCOPES` (space-separated) for staging/local
27
+ overrides. Defaults match the production deployment.
28
+ - **Campaign emulator/proxy/schedule parity.** `create_campaign` and
29
+ `update_campaign` now expose the full device + proxy + schedule
30
+ surface the API has always supported: `emulator_specific_ids`
31
+ (pin exact device slugs, e.g.
32
+ `samsung_galaxy_s23_ultra_android16`), `emulator_mode`
33
+ (`random` = 1 device per category, `all` = every device),
34
+ `proxy_type`/`proxy_region`/`proxy_city`/`proxy_isp`, and a real
35
+ schedule definition (`schedule_type` `weekly`/`interval`,
36
+ `schedule_weekly`, `schedule_interval_seconds`,
37
+ `schedule_timezone`). `update_campaign` additionally accepts `url`,
38
+ `ad_tag`, and `group_id`. Campaign read responses
39
+ (`get_campaign` / `list_campaigns`) now include `emulator_selection`,
40
+ the proxy fields, and the detailed schedule.
41
+ - **Scan proxy targeting.** `create_scan` and `create_bulk_scans`
42
+ accept an optional `proxy` object (`proxy_type` + `region` / `city`
43
+ / `isp`).
44
+ - **`update_webhook`** now accepts `description`, `campaign_ids`, and
45
+ `clear_campaign_ids` (relabel an endpoint or change its
46
+ campaign-restriction after creation).
47
+ - **`get_billing_summary`** now surfaces `credit_limit_micros`,
48
+ `effective_minimum_balance_micros`, `current_plan_is_custom`, and
49
+ the scheduled-plan-change fields (`scheduled_next_plan_id`,
50
+ `scheduled_next_plan_name`, `scheduled_effective_at`).
51
+ - **`invite_user`** accepts an optional `timezone`; `list_org_users`
52
+ now returns each member's `role_id` (so `update_user_role` no
53
+ longer needs a guessed UUID).
54
+
55
+ ### Removed
56
+
57
+ - **Phantom tool inputs that the API silently ignored.**
58
+ `update_campaign_group` no longer advertises `schedule_paused`
59
+ (use `pause_campaign_group_schedule` /
60
+ `resume_campaign_group_schedule` instead), and `update_org` no
61
+ longer advertises `settings` (the API accepts only `name`). Both
62
+ were no-ops; removing them is non-breaking (unknown keys were
63
+ already stripped by input validation).
64
+
65
+ ## [0.3.0] - 2026-05-20
66
+
67
+ Major-feature release — full parity with the API surface added in
68
+ COOP-13940 Phase 3 (custom taxonomies + 5-kind policy rules + binary
69
+ downloads). **Contains breaking changes for existing agents** —
70
+ review the migration notes below before upgrading.
71
+
72
+ ### Breaking
73
+
74
+ - **`update_tag_definition`**: input field `show_in_public_report`
75
+ (boolean) was removed; replaced by `visibility` (enum:
76
+ `hidden | internal | public`). Old payloads now fail zod
77
+ validation locally with a typed `invalid-input` error before
78
+ hitting the API. Migration: replace `show_in_public_report: true`
79
+ → `visibility: "public"`, and `show_in_public_report: false` →
80
+ `visibility: "internal"` (or `"hidden"` if you want the tag
81
+ fully suppressed).
82
+ - **`list_alerts`**: `status` filter no longer accepts the legacy
83
+ values `"ack"` / `"ignored"`. The canonical four are
84
+ `"open"`, `"acknowledged"`, `"resolved"`, `"dismissed"`. Mapping:
85
+ `"ack"` → `"acknowledged"`, `"ignored"` → `"dismissed"`.
86
+ - **`create_policy_set` / `update_policy_set`**: `entries[]` is now
87
+ a discriminated union over **five rule kinds**. Existing tag-only
88
+ callers MUST add `rule_type: "tag"` to every entry. Other kinds:
89
+ `"iab_v3"` (with `iab_v3: { tier1, tier2?, tier3?, tier4? }`),
90
+ `"brand"` (with `brand: string`), `"ai_category"` (with
91
+ `ai_category: { tier1, tier2?, tier3?, tier4? }`),
92
+ `"custom_taxonomy"` (with `custom_taxonomy: { taxonomy_id, tier1,
93
+ tier2?, tier3?, tier4? }`).
94
+ `country_codes` now accepts both ISO alpha-2 and alpha-3 (the
95
+ API normalises).
96
+
97
+ ### Added
98
+
99
+ - **`custom_taxonomies` tool suite (7 tools).** Per-org
100
+ classification trees with an `is_default` fallback node:
101
+ `list_custom_taxonomies`, `get_custom_taxonomy`,
102
+ `create_custom_taxonomy`, `update_custom_taxonomy`,
103
+ `delete_custom_taxonomy` (soft-delete), `restore_custom_taxonomy`,
104
+ `parse_custom_taxonomy_text` (no-persistence preview).
105
+ - **Account labels.** `list_account_labels` + `update_account_labels`
106
+ for the per-org metadata schema that backs `list_scans.labels`
107
+ filters. `update_account_labels` is hinted destructive — passing
108
+ `labels: []` wipes the catalogue.
109
+ - **`create_custom_role`** for `/api/v1/account/roles` POST.
110
+ - **Binary downloads** — `get_invoice_pdf` (MCP `resource` block),
111
+ `get_scan_screenshot`, `get_scan_creative_screenshot`,
112
+ `get_scan_landing_screenshot` (MCP `image` blocks). All four
113
+ return inline base64 with the API-supplied `mimeType` so agents
114
+ can save / forward without a second call.
115
+ - **`list_alerts` rule_type + matched_value.** Each alert row now
116
+ carries `rule_type` (one of the five kinds) and `matched_value`
117
+ (the canonical text the scan matched against). Lets agents
118
+ branch on rule kind without re-fetching the scan.
119
+ - **`list_policy_sets.visibility` filter.** Narrow to
120
+ `private` (org-owned) or `public` (Kaminari Ad-curated). Omit
121
+ the filter to see both combined.
122
+ - **`list_scans` filters.** Added `run_id`, `campaign_id`,
123
+ `group_id`, `timezone`, `ai_category`, `iab_v3_category`,
124
+ `iab_category`, `brand`, plus a typed `labels` record that
125
+ expands to `label_<key>=<value>` query params (snake_case keys
126
+ enforced by zod).
127
+ - **Misc query filters.** `list_campaigns` + `list_campaigns_picker`
128
+ gained `archived` / `q`; `list_invoices` gained `type` / `status`
129
+ enums; `list_balance_history` gained multi-select `type`;
130
+ `list_webhook_deliveries` gained `success` / `from_ts` / `to_ts`;
131
+ `list_usage` switched to ISO 8601 datetime bounds; `list_tags`
132
+ gained `category`.
133
+
134
+ ### Changed
135
+
136
+ - **OpenAPI types regenerated.** `src/shared/api/openapi.ts` and
137
+ `src/shared/api/zod-schemas.ts` are rebuilt from the prod API and
138
+ capture every Phase 3 schema (custom-taxonomies, 5-kind
139
+ PolicyEntry, AlertResponse `rule_type` / `matched_value`, tag
140
+ visibility rename).
141
+ - **Tool descriptions.** Policy-sets, alerts, and tag tools updated
142
+ to mention the new fields. `get_alert_stats` description uses the
143
+ canonical AlertStatus names.
144
+ - **Branding.** All user-visible tool descriptions and JSDoc use the
145
+ canonical `Kaminari Ad` (with space) per the updated branding
146
+ rule. The dotted form `Kaminari.Ad` is deprecated.
147
+
148
+ ### Quality gates
149
+
150
+ - 705 unit tests (was 623). Coverage 100% statements / 100% lines /
151
+ 100% functions / 97.89% branches (gate is 95% branches). Every
152
+ new tool has at least 3 cases (success / api-error /
153
+ zod-rejection); policy-set tools cover all five rule kinds with
154
+ per-kind missing-block rejection tests.
155
+ - All existing gates pass: lint, format, typecheck, tool-naming,
156
+ file-sizes (8 grandfathered, every new file <200 effective
157
+ lines), check-imports, check-shared-state,
158
+ check-no-handwritten-parsers.
159
+
10
160
  ## [0.2.2] - 2026-05-20
11
161
 
12
162
  Minor release — adds one-click install paths for Cursor and Claude
package/README.md CHANGED
@@ -82,6 +82,33 @@ For cloud agents or clients without a local Node runtime, point at the hosted en
82
82
  }
83
83
  ```
84
84
 
85
+ ### 2c. OAuth 2.0 (Claude directory, third-party agents)
86
+
87
+ The hosted server publishes RFC 9728 protected-resource metadata at
88
+ [`https://mcp.kaminari.ad/.well-known/oauth-protected-resource`](https://mcp.kaminari.ad/.well-known/oauth-protected-resource)
89
+ and points at the Kaminari Ad Authorization Server
90
+ (`https://app.kaminari.ad`). Any unauthenticated request to `/mcp`
91
+ returns a `WWW-Authenticate: Bearer resource_metadata="…"` header so
92
+ spec-compliant MCP clients (Claude.ai, Claude Code, third-party
93
+ agents) can complete an OAuth 2.0 authorization-code flow with PKCE
94
+ S256 + Dynamic Client Registration (RFC 7591).
95
+
96
+ ```bash
97
+ # Discovery — works without any credential
98
+ curl -sk https://mcp.kaminari.ad/.well-known/oauth-protected-resource
99
+
100
+ # Triggering the WWW-Authenticate hint
101
+ curl -isk https://mcp.kaminari.ad/mcp -X POST \
102
+ -H 'content-type: application/json' \
103
+ -d '{"jsonrpc":"2.0","method":"tools/list","id":1}'
104
+ ```
105
+
106
+ **API keys remain the recommended path for CLIs and one-off
107
+ scripting** — OAuth is only for interactive agents that want per-app
108
+ consent and per-app revocation. Both Bearer flavours hit the same
109
+ `/mcp` endpoint; the server forwards the token verbatim to the API,
110
+ which decides which credential type minted it.
111
+
85
112
  ---
86
113
 
87
114
  ## Tools
package/dist/bin.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { NAME, VERSION, err, ok } from './chunk-BAZQPU6T.js';
2
+ import { NAME, VERSION, err, ok } from './chunk-RBCMUVI6.js';
3
3
  import process from 'process';
4
4
  import { z } from 'zod';
5
5
 
@@ -27,6 +27,34 @@ function checkNodeVersion(nodeVersion) {
27
27
  }
28
28
  return ok(void 0);
29
29
  }
30
+
31
+ // src/shared/oauth-scope-catalogue.ts
32
+ var DEFAULT_OAUTH_SCOPES = Object.freeze([
33
+ "mcp:account:read",
34
+ "mcp:alert_notifications:read",
35
+ "mcp:alert_notifications:write",
36
+ "mcp:alerts:read",
37
+ "mcp:alerts:write",
38
+ "mcp:billing:read",
39
+ "mcp:campaigns:read",
40
+ "mcp:campaigns:write",
41
+ "mcp:custom_rules:read",
42
+ "mcp:custom_rules:write",
43
+ "mcp:invoicing:read",
44
+ "mcp:policies:read",
45
+ "mcp:policies:write",
46
+ "mcp:scans:read",
47
+ "mcp:scans:write",
48
+ "mcp:tags:read",
49
+ "mcp:tags:write",
50
+ "mcp:taxonomies:read",
51
+ "mcp:taxonomies:write",
52
+ "mcp:webhooks:read",
53
+ "mcp:webhooks:write",
54
+ "offline_access"
55
+ ]);
56
+
57
+ // src/shared/config.ts
30
58
  var TransportSchema = z.enum(["stdio", "http"]);
31
59
  var LogLevelSchema = z.enum(["trace", "debug", "info", "warn", "error", "fatal"]);
32
60
  var LogFormatSchema = z.enum(["pretty", "json"]);
@@ -43,7 +71,16 @@ var RawSchema = z.object({
43
71
  KAMINARI_AD_HTTP_PORT: z.coerce.number().int().min(0).max(65535).default(8080),
44
72
  KAMINARI_AD_SESSION_TTL_SEC: z.coerce.number().int().min(60).max(86400).default(1800),
45
73
  KAMINARI_AD_RATE_LIMIT_RPM: z.coerce.number().int().min(1).max(1e4).default(120),
46
- KAMINARI_AD_API_KEY: z.string().min(8).optional()
74
+ KAMINARI_AD_API_KEY: z.string().min(8).optional(),
75
+ // OAuth discovery (RFC 9728 protected-resource + RFC 8414 AS
76
+ // metadata). All three URLs default to the production deployment;
77
+ // local/staging operators override per-environment.
78
+ KAMINARI_AD_OAUTH_PROTECTED_RESOURCE: z.string().url().default("https://mcp.kaminari.ad/mcp"),
79
+ KAMINARI_AD_OAUTH_PROTECTED_RESOURCE_METADATA_URL: z.string().url().default("https://mcp.kaminari.ad/.well-known/oauth-protected-resource"),
80
+ KAMINARI_AD_OAUTH_AUTHORIZATION_SERVER_URL: z.string().url().default("https://app.kaminari.ad"),
81
+ // Space-separated scope list. We split on /\s+/ post-parse and drop
82
+ // empty tokens so accidental double-spaces don't corrupt the array.
83
+ KAMINARI_AD_OAUTH_SCOPES: z.string().optional()
47
84
  });
48
85
  function loadConfig(env) {
49
86
  const parsed = RawSchema.safeParse(env);
@@ -53,6 +90,8 @@ function loadConfig(env) {
53
90
  const raw = parsed.data;
54
91
  const transport = raw.KAMINARI_AD_TRANSPORT;
55
92
  const logFormat = raw.KAMINARI_AD_LOG_FORMAT ?? (transport === "stdio" ? "pretty" : "json");
93
+ const oauthScopesRaw = raw.KAMINARI_AD_OAUTH_SCOPES;
94
+ const oauthScopes = oauthScopesRaw === void 0 ? DEFAULT_OAUTH_SCOPES : Object.freeze(oauthScopesRaw.split(/\s+/).filter((s) => s.length > 0));
56
95
  return ok({
57
96
  transport,
58
97
  apiBaseUrl: raw.KAMINARI_AD_API_URL,
@@ -61,7 +100,11 @@ function loadConfig(env) {
61
100
  httpPort: raw.KAMINARI_AD_HTTP_PORT,
62
101
  sessionTtlSec: raw.KAMINARI_AD_SESSION_TTL_SEC,
63
102
  rateLimitRpm: raw.KAMINARI_AD_RATE_LIMIT_RPM,
64
- stdioApiKey: raw.KAMINARI_AD_API_KEY
103
+ stdioApiKey: raw.KAMINARI_AD_API_KEY,
104
+ oauthProtectedResource: raw.KAMINARI_AD_OAUTH_PROTECTED_RESOURCE,
105
+ oauthProtectedResourceMetadataUrl: raw.KAMINARI_AD_OAUTH_PROTECTED_RESOURCE_METADATA_URL,
106
+ oauthAuthorizationServerUrl: raw.KAMINARI_AD_OAUTH_AUTHORIZATION_SERVER_URL,
107
+ oauthScopes
65
108
  });
66
109
  }
67
110
 
@@ -119,10 +162,10 @@ async function main() {
119
162
  }
120
163
  const config = configResult.value;
121
164
  if (config.transport === "stdio") {
122
- const { bootstrapStdio } = await import('./stdio-bootstrap-PSX3TI3I.js');
165
+ const { bootstrapStdio } = await import('./stdio-bootstrap-7GVABHZQ.js');
123
166
  return bootstrapStdio(config);
124
167
  }
125
- const { bootstrapHttp } = await import('./http-bootstrap-PW4K2NDQ.js');
168
+ const { bootstrapHttp } = await import('./http-bootstrap-TGX6STYD.js');
126
169
  return bootstrapHttp(config);
127
170
  }
128
171
  main().then(
package/dist/bin.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/shared/check-node-version.ts","../src/shared/config.ts","../src/bin.ts"],"names":[],"mappings":"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","file":"bin.js","sourcesContent":["/**\n * Pure Node-version preflight, extracted from {@link bin.ts} so it\n * can be unit-tested without `process.exit` side effects.\n *\n * Why this exists: `undici@8.x` removed feature probes in v8.0.3 and\n * imports `markAsUncloneable` from `node:worker_threads`\n * unconditionally — that symbol only exists on Node 22.19+. Older\n * Node versions crash at import time with the cryptic message\n * `webidl.util.markAsUncloneable is not a function`. We surface a\n * clean upgrade message instead, BEFORE any dynamic import pulls\n * undici.\n *\n * Keep {@link REQUIRED_NODE_MAJOR} / {@link REQUIRED_NODE_MINOR} in\n * sync with `engines.node` in `package.json` (currently\n * `>=22.19.0`).\n */\n\nimport { err, ok, type Result } from \"./result.js\";\nimport { NAME } from \"./version.js\";\n\nexport const REQUIRED_NODE_MAJOR = 22;\nexport const REQUIRED_NODE_MINOR = 19;\n\n/**\n * Check whether `nodeVersion` (e.g. `\"22.19.0\"`, the value of\n * `process.versions.node`) satisfies the package's runtime floor.\n *\n * Returns `Ok(undefined)` on pass, `Err(message)` with a multi-line\n * upgrade message on fail. The message is plain text ready to be\n * written to stderr verbatim.\n */\nexport function checkNodeVersion(nodeVersion: string): Result<undefined, string> {\n const parts = nodeVersion.split(\".\");\n const major = Number(parts[0]);\n const minor = Number(parts[1]);\n if (\n Number.isNaN(major) ||\n Number.isNaN(minor) ||\n major < REQUIRED_NODE_MAJOR ||\n (major === REQUIRED_NODE_MAJOR && minor < REQUIRED_NODE_MINOR)\n ) {\n const required = `>=${String(REQUIRED_NODE_MAJOR)}.${String(REQUIRED_NODE_MINOR)}.0`;\n return err(\n [\n `${NAME} requires Node.js ${required} (you have v${nodeVersion}).`,\n \"\",\n \"The underlying undici 8.x HTTP client uses markAsUncloneable\",\n \"from node:worker_threads, available only on Node 22.19+.\",\n \"Older Node crashes at import time with the cryptic message\",\n \"`webidl.util.markAsUncloneable is not a function`.\",\n \"\",\n \"Please upgrade Node and re-run: https://nodejs.org/en/download\",\n ].join(\"\\n\")\n );\n }\n return ok(undefined);\n}\n","/**\n * Runtime configuration for `@kaminari-ad/mcp`.\n *\n * The single source of truth for all env-var reads. `process.env.X`\n * outside this file is a code-review blocker — every config value flows\n * through the zod-parsed {@link Config} object.\n */\n\nimport { z } from \"zod\";\n\nimport { err, ok, type Result } from \"./result.js\";\n\n/**\n * MCP transport. `stdio` is for local install (one process per user);\n * `http` is for the hosted multi-tenant endpoint.\n */\nexport const TransportSchema = z.enum([\"stdio\", \"http\"]);\nexport type Transport = z.infer<typeof TransportSchema>;\n\n/**\n * pino log level.\n */\nexport const LogLevelSchema = z.enum([\"trace\", \"debug\", \"info\", \"warn\", \"error\", \"fatal\"]);\nexport type LogLevel = z.infer<typeof LogLevelSchema>;\n\n/**\n * pino log output format.\n *\n * - `pretty` — colorised single-line ANSI output for terminals. Default\n * in stdio mode (operator reads stderr in their shell).\n * - `json` — newline-delimited JSON, one record per line. Default in\n * HTTP mode (Docker/k8s log aggregators expect JSON). MCP hosts that\n * capture stderr from the stdio process (Cursor, Claude Desktop)\n * parse much better with `json` — flip this when the operator is a\n * machine, not a human.\n */\nexport const LogFormatSchema = z.enum([\"pretty\", \"json\"]);\nexport type LogFormat = z.infer<typeof LogFormatSchema>;\n\n/**\n * Parsed configuration. All fields are `readonly` — mutation post-load\n * is a bug.\n */\nexport interface Config {\n readonly transport: Transport;\n readonly apiBaseUrl: string;\n readonly logLevel: LogLevel;\n readonly logFormat: LogFormat;\n readonly httpPort: number;\n readonly sessionTtlSec: number;\n readonly rateLimitRpm: number;\n /**\n * Set only in stdio mode. In HTTP mode the bootstrap MUST assert\n * this is `undefined` (see CONTRIBUTING.md \"Tenant isolation\" §5).\n */\n readonly stdioApiKey: string | undefined;\n}\n\n/**\n * Every env var carries the `KAMINARI_AD_` namespace prefix.\n *\n * Why: this process inherits the host's full environment (Cursor /\n * Claude Desktop / Docker compose / k8s pod), where `API_BASE_URL` /\n * `LOG_LEVEL` / `HTTP_PORT` are common generic names. Without a\n * namespace, another tool in the same shell trivially poisons our\n * config — e.g. `LOG_LEVEL=debug` set for some app library suddenly\n * makes the MCP server log every secret-redacted-but-still-noisy\n * request to stderr, and the user has no idea why. With the prefix\n * the boundary is explicit and `env | grep KAMINARI_AD_` enumerates\n * exactly what we read.\n */\nconst RawSchema = z.object({\n KAMINARI_AD_TRANSPORT: TransportSchema.default(\"stdio\"),\n // `app.kaminari.ad` is the API host; `kaminari.ad` (no subdomain)\n // is the marketing landing page and does NOT serve /api/v1/*.\n // Setting this wrong (e.g. the root domain) makes every tool call\n // fail with HTTP 404 — the symptom that surfaced this default bug\n // in v0.2.0.\n KAMINARI_AD_API_URL: z.string().url().default(\"https://app.kaminari.ad\"),\n KAMINARI_AD_LOG_LEVEL: LogLevelSchema.default(\"info\"),\n KAMINARI_AD_LOG_FORMAT: LogFormatSchema.optional(),\n KAMINARI_AD_HTTP_PORT: z.coerce.number().int().min(0).max(65535).default(8080),\n KAMINARI_AD_SESSION_TTL_SEC: z.coerce.number().int().min(60).max(86_400).default(1800),\n KAMINARI_AD_RATE_LIMIT_RPM: z.coerce.number().int().min(1).max(10_000).default(120),\n KAMINARI_AD_API_KEY: z.string().min(8).optional(),\n});\n\n/**\n * Parse and validate environment variables into a {@link Config}.\n *\n * Returns `Err<ConfigError>` on validation failure. Caller (typically\n * `bin.ts`) prints the error and exits non-zero.\n *\n * `KAMINARI_AD_LOG_FORMAT` defaults are transport-dependent: `pretty`\n * for stdio (terminal-facing) and `json` for http (aggregator-facing).\n * If the user sets the env var explicitly, that wins.\n */\nexport function loadConfig(env: NodeJS.ProcessEnv): Result<Config, ConfigError> {\n const parsed = RawSchema.safeParse(env);\n if (!parsed.success) {\n return err({ kind: \"invalid\", issues: parsed.error.flatten().fieldErrors });\n }\n const raw = parsed.data;\n const transport = raw.KAMINARI_AD_TRANSPORT;\n const logFormat = raw.KAMINARI_AD_LOG_FORMAT ?? (transport === \"stdio\" ? \"pretty\" : \"json\");\n return ok({\n transport,\n apiBaseUrl: raw.KAMINARI_AD_API_URL,\n logLevel: raw.KAMINARI_AD_LOG_LEVEL,\n logFormat,\n httpPort: raw.KAMINARI_AD_HTTP_PORT,\n sessionTtlSec: raw.KAMINARI_AD_SESSION_TTL_SEC,\n rateLimitRpm: raw.KAMINARI_AD_RATE_LIMIT_RPM,\n stdioApiKey: raw.KAMINARI_AD_API_KEY,\n });\n}\n\n/**\n * Errors from {@link loadConfig}.\n */\nexport interface ConfigError {\n readonly kind: \"invalid\";\n readonly issues: Readonly<Record<string, readonly string[] | undefined>>;\n}\n","/**\n * CLI entrypoint for `@kaminari-ad/mcp`.\n *\n * Dispatches to a transport-specific composition root (stdio or HTTP)\n * based on the `--transport` flag (or `TRANSPORT` env var, with stdio\n * as the default).\n *\n * Wire-up is intentionally minimal — every dependency is constructed\n * inside the chosen bootstrap, never here. This file just parses argv\n * and config, then hands off.\n *\n * Imports policy: the STATIC imports below MUST stay free of any\n * runtime dependency that requires Node >= 22.19 (undici, MCP SDK,\n * pino). Otherwise the Node-version preflight in `main()` runs too\n * late — those modules crash at import time on older Node. The\n * transport bootstraps pull undici / MCP SDK / pino via dynamic\n * `await import()` so the preflight gets to fire first.\n */\n\nimport process from \"node:process\";\n\nimport { checkNodeVersion } from \"./shared/check-node-version.js\";\nimport { loadConfig, type Transport } from \"./shared/config.js\";\nimport { NAME, VERSION } from \"./shared/version.js\";\n\nfunction parseTransportFlag(argv: readonly string[]): Transport | undefined {\n for (const arg of argv) {\n if (arg === \"--transport=stdio\" || arg === \"--transport=http\") {\n return arg === \"--transport=stdio\" ? \"stdio\" : \"http\";\n }\n if (arg.startsWith(\"--transport=\")) {\n const value = arg.slice(\"--transport=\".length);\n if (value === \"stdio\" || value === \"http\") return value;\n }\n }\n return undefined;\n}\n\nasync function main(): Promise<number> {\n const argv = process.argv.slice(2);\n\n // Informational flags handled BEFORE the Node preflight so users on\n // any Node can confirm the binary resolves and discover the help\n // text — the only paths that touch zero runtime deps.\n if (argv.includes(\"--version\") || argv.includes(\"-v\")) {\n process.stdout.write(`${NAME} ${VERSION}\\n`);\n return 0;\n }\n if (argv.includes(\"--help\") || argv.includes(\"-h\")) {\n process.stdout.write(\n [\n `${NAME} ${VERSION}`,\n ``,\n `Usage:`,\n ` kaminari-ad-mcp [--transport=stdio|http]`,\n ``,\n `Transports:`,\n ` stdio (default) Local install — auth via KAMINARI_AD_API_KEY env var.`,\n ` http Hosted multi-tenant server — auth via incoming Authorization header.`,\n ``,\n `Configuration is documented in .env.example.`,\n ``,\n ].join(\"\\n\")\n );\n return 0;\n }\n\n // Preflight: bail with a clean upgrade message BEFORE any dynamic\n // import pulls undici / MCP SDK / pino — all three require\n // Node 22.19+ and crash at import time on older Node. Without this\n // gate users see undici's cryptic\n // `webidl.util.markAsUncloneable is not a function` instead.\n const check = checkNodeVersion(process.versions.node);\n if (check.isErr()) {\n process.stderr.write(`${check.error}\\n`);\n return 2;\n }\n\n const cliTransport = parseTransportFlag(argv);\n const env: NodeJS.ProcessEnv = cliTransport\n ? { ...process.env, KAMINARI_AD_TRANSPORT: cliTransport }\n : process.env;\n\n const configResult = loadConfig(env);\n if (configResult.isErr()) {\n process.stderr.write(`Invalid configuration: ${JSON.stringify(configResult.error.issues)}\\n`);\n return 2;\n }\n const config = configResult.value;\n\n if (config.transport === \"stdio\") {\n const { bootstrapStdio } = await import(\"./presentation/stdio/stdio-bootstrap.js\");\n return bootstrapStdio(config);\n }\n const { bootstrapHttp } = await import(\"./presentation/http/http-bootstrap.js\");\n return bootstrapHttp(config);\n}\n\nmain().then(\n (code) => {\n process.exit(code);\n },\n (error: unknown) => {\n process.stderr.write(`Fatal: ${error instanceof Error ? error.message : String(error)}\\n`);\n process.exit(1);\n }\n);\n"]}
1
+ {"version":3,"sources":["../src/shared/check-node-version.ts","../src/shared/oauth-scope-catalogue.ts","../src/shared/config.ts","../src/bin.ts"],"names":[],"mappings":"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","file":"bin.js","sourcesContent":["/**\n * Pure Node-version preflight, extracted from {@link bin.ts} so it\n * can be unit-tested without `process.exit` side effects.\n *\n * Why this exists: `undici@8.x` removed feature probes in v8.0.3 and\n * imports `markAsUncloneable` from `node:worker_threads`\n * unconditionally — that symbol only exists on Node 22.19+. Older\n * Node versions crash at import time with the cryptic message\n * `webidl.util.markAsUncloneable is not a function`. We surface a\n * clean upgrade message instead, BEFORE any dynamic import pulls\n * undici.\n *\n * Keep {@link REQUIRED_NODE_MAJOR} / {@link REQUIRED_NODE_MINOR} in\n * sync with `engines.node` in `package.json` (currently\n * `>=22.19.0`).\n */\n\nimport { err, ok, type Result } from \"./result.js\";\nimport { NAME } from \"./version.js\";\n\nexport const REQUIRED_NODE_MAJOR = 22;\nexport const REQUIRED_NODE_MINOR = 19;\n\n/**\n * Check whether `nodeVersion` (e.g. `\"22.19.0\"`, the value of\n * `process.versions.node`) satisfies the package's runtime floor.\n *\n * Returns `Ok(undefined)` on pass, `Err(message)` with a multi-line\n * upgrade message on fail. The message is plain text ready to be\n * written to stderr verbatim.\n */\nexport function checkNodeVersion(nodeVersion: string): Result<undefined, string> {\n const parts = nodeVersion.split(\".\");\n const major = Number(parts[0]);\n const minor = Number(parts[1]);\n if (\n Number.isNaN(major) ||\n Number.isNaN(minor) ||\n major < REQUIRED_NODE_MAJOR ||\n (major === REQUIRED_NODE_MAJOR && minor < REQUIRED_NODE_MINOR)\n ) {\n const required = `>=${String(REQUIRED_NODE_MAJOR)}.${String(REQUIRED_NODE_MINOR)}.0`;\n return err(\n [\n `${NAME} requires Node.js ${required} (you have v${nodeVersion}).`,\n \"\",\n \"The underlying undici 8.x HTTP client uses markAsUncloneable\",\n \"from node:worker_threads, available only on Node 22.19+.\",\n \"Older Node crashes at import time with the cryptic message\",\n \"`webidl.util.markAsUncloneable is not a function`.\",\n \"\",\n \"Please upgrade Node and re-run: https://nodejs.org/en/download\",\n ].join(\"\\n\")\n );\n }\n return ok(undefined);\n}\n","/**\n * Canonical catalogue of OAuth 2.0 scopes the Kaminari Ad MCP server\n * advertises in its protected-resource metadata (RFC 9728) and\n * `WWW-Authenticate: Bearer scope=\"...\"` challenge.\n *\n * The catalogue is intentionally static and lives in `shared/` because\n * (a) it has no runtime behaviour — it is data — and (b) it is read by\n * both the `protected-resource-metadata-handler` and the\n * `www-authenticate` builder. If a new scope is added on the\n * Authorization Server side, mirror it here in the same release so the\n * Resource Server (this process) advertises it.\n *\n * `offline_access` is part of the catalogue per RFC 6749 §6 because\n * the Authorization Server issues refresh tokens only when the client\n * requests this scope. Anthropic's Claude clients append it\n * automatically when the server advertises it in\n * `scopes_supported`/the `WWW-Authenticate` `scope=` parameter.\n */\n\n/**\n * Default scope catalogue, frozen at build time.\n *\n * MUST stay byte-identical to the Authorization Server's advertised\n * catalogue (`app.oauth_authorization.domain.entities.scope.advertised_scopes()`\n * — i.e. `sorted(KNOWN_SCOPES)`). The order is lexicographic, which\n * happens to yield the desired semantics for free: `:read` sorts before\n * `:write` within a resource, and `offline_access` sorts after every\n * `mcp:` scope.\n *\n * Read scopes grant the resource's view permission; write scopes grant\n * read + the resource's mutating permissions. billing/invoicing/account\n * are read-only over OAuth by design (money-spend / privilege-escalation\n * tools stay API-key only). When the AS catalogue changes, mirror it\n * here in the same release — `tests/unit/shared/oauth-scope-catalogue.test.ts`\n * pins the exact list so drift fails CI.\n */\nexport const DEFAULT_OAUTH_SCOPES: readonly string[] = Object.freeze([\n \"mcp:account:read\",\n \"mcp:alert_notifications:read\",\n \"mcp:alert_notifications:write\",\n \"mcp:alerts:read\",\n \"mcp:alerts:write\",\n \"mcp:billing:read\",\n \"mcp:campaigns:read\",\n \"mcp:campaigns:write\",\n \"mcp:custom_rules:read\",\n \"mcp:custom_rules:write\",\n \"mcp:invoicing:read\",\n \"mcp:policies:read\",\n \"mcp:policies:write\",\n \"mcp:scans:read\",\n \"mcp:scans:write\",\n \"mcp:tags:read\",\n \"mcp:tags:write\",\n \"mcp:taxonomies:read\",\n \"mcp:taxonomies:write\",\n \"mcp:webhooks:read\",\n \"mcp:webhooks:write\",\n \"offline_access\",\n]);\n","/**\n * Runtime configuration for `@kaminari-ad/mcp`.\n *\n * The single source of truth for all env-var reads. `process.env.X`\n * outside this file is a code-review blocker — every config value flows\n * through the zod-parsed {@link Config} object.\n */\n\nimport { z } from \"zod\";\n\nimport { DEFAULT_OAUTH_SCOPES } from \"./oauth-scope-catalogue.js\";\nimport { err, ok, type Result } from \"./result.js\";\n\n/**\n * MCP transport. `stdio` is for local install (one process per user);\n * `http` is for the hosted multi-tenant endpoint.\n */\nexport const TransportSchema = z.enum([\"stdio\", \"http\"]);\nexport type Transport = z.infer<typeof TransportSchema>;\n\n/**\n * pino log level.\n */\nexport const LogLevelSchema = z.enum([\"trace\", \"debug\", \"info\", \"warn\", \"error\", \"fatal\"]);\nexport type LogLevel = z.infer<typeof LogLevelSchema>;\n\n/**\n * pino log output format.\n *\n * - `pretty` — colorised single-line ANSI output for terminals. Default\n * in stdio mode (operator reads stderr in their shell).\n * - `json` — newline-delimited JSON, one record per line. Default in\n * HTTP mode (Docker/k8s log aggregators expect JSON). MCP hosts that\n * capture stderr from the stdio process (Cursor, Claude Desktop)\n * parse much better with `json` — flip this when the operator is a\n * machine, not a human.\n */\nexport const LogFormatSchema = z.enum([\"pretty\", \"json\"]);\nexport type LogFormat = z.infer<typeof LogFormatSchema>;\n\n/**\n * Parsed configuration. All fields are `readonly` — mutation post-load\n * is a bug.\n */\nexport interface Config {\n readonly transport: Transport;\n readonly apiBaseUrl: string;\n readonly logLevel: LogLevel;\n readonly logFormat: LogFormat;\n readonly httpPort: number;\n readonly sessionTtlSec: number;\n readonly rateLimitRpm: number;\n /**\n * Set only in stdio mode. In HTTP mode the bootstrap MUST assert\n * this is `undefined` (see CONTRIBUTING.md \"Tenant isolation\" §5).\n */\n readonly stdioApiKey: string | undefined;\n /**\n * Canonical MCP endpoint URL advertised as the `resource` field of\n * the RFC 9728 protected-resource metadata document. Anthropic's\n * Claude clients require this to match the URL the user enters in\n * Claude exactly (including the path component).\n */\n readonly oauthProtectedResource: string;\n /**\n * Absolute URL where this server serves the protected-resource\n * metadata JSON. Advertised back to clients via\n * `WWW-Authenticate: Bearer resource_metadata=\"…\"` on 401.\n */\n readonly oauthProtectedResourceMetadataUrl: string;\n /**\n * Issuer URL of the Authorization Server that mints access tokens\n * accepted by this Resource Server. Advertised as the first (and\n * only) entry of the metadata's `authorization_servers` array.\n */\n readonly oauthAuthorizationServerUrl: string;\n /**\n * Scopes the Resource Server advertises in both the metadata's\n * `scopes_supported` field and the 401 `WWW-Authenticate scope=…`\n * parameter. Defaults to {@link DEFAULT_OAUTH_SCOPES}; operators\n * may override via `KAMINARI_AD_OAUTH_SCOPES` (space-separated).\n */\n readonly oauthScopes: readonly string[];\n}\n\n/**\n * Every env var carries the `KAMINARI_AD_` namespace prefix.\n *\n * Why: this process inherits the host's full environment (Cursor /\n * Claude Desktop / Docker compose / k8s pod), where `API_BASE_URL` /\n * `LOG_LEVEL` / `HTTP_PORT` are common generic names. Without a\n * namespace, another tool in the same shell trivially poisons our\n * config — e.g. `LOG_LEVEL=debug` set for some app library suddenly\n * makes the MCP server log every secret-redacted-but-still-noisy\n * request to stderr, and the user has no idea why. With the prefix\n * the boundary is explicit and `env | grep KAMINARI_AD_` enumerates\n * exactly what we read.\n */\nconst RawSchema = z.object({\n KAMINARI_AD_TRANSPORT: TransportSchema.default(\"stdio\"),\n // `app.kaminari.ad` is the API host; `kaminari.ad` (no subdomain)\n // is the marketing landing page and does NOT serve /api/v1/*.\n // Setting this wrong (e.g. the root domain) makes every tool call\n // fail with HTTP 404 — the symptom that surfaced this default bug\n // in v0.2.0.\n KAMINARI_AD_API_URL: z.string().url().default(\"https://app.kaminari.ad\"),\n KAMINARI_AD_LOG_LEVEL: LogLevelSchema.default(\"info\"),\n KAMINARI_AD_LOG_FORMAT: LogFormatSchema.optional(),\n KAMINARI_AD_HTTP_PORT: z.coerce.number().int().min(0).max(65535).default(8080),\n KAMINARI_AD_SESSION_TTL_SEC: z.coerce.number().int().min(60).max(86_400).default(1800),\n KAMINARI_AD_RATE_LIMIT_RPM: z.coerce.number().int().min(1).max(10_000).default(120),\n KAMINARI_AD_API_KEY: z.string().min(8).optional(),\n // OAuth discovery (RFC 9728 protected-resource + RFC 8414 AS\n // metadata). All three URLs default to the production deployment;\n // local/staging operators override per-environment.\n KAMINARI_AD_OAUTH_PROTECTED_RESOURCE: z.string().url().default(\"https://mcp.kaminari.ad/mcp\"),\n KAMINARI_AD_OAUTH_PROTECTED_RESOURCE_METADATA_URL: z\n .string()\n .url()\n .default(\"https://mcp.kaminari.ad/.well-known/oauth-protected-resource\"),\n KAMINARI_AD_OAUTH_AUTHORIZATION_SERVER_URL: z.string().url().default(\"https://app.kaminari.ad\"),\n // Space-separated scope list. We split on /\\s+/ post-parse and drop\n // empty tokens so accidental double-spaces don't corrupt the array.\n KAMINARI_AD_OAUTH_SCOPES: z.string().optional(),\n});\n\n/**\n * Parse and validate environment variables into a {@link Config}.\n *\n * Returns `Err<ConfigError>` on validation failure. Caller (typically\n * `bin.ts`) prints the error and exits non-zero.\n *\n * `KAMINARI_AD_LOG_FORMAT` defaults are transport-dependent: `pretty`\n * for stdio (terminal-facing) and `json` for http (aggregator-facing).\n * If the user sets the env var explicitly, that wins.\n */\nexport function loadConfig(env: NodeJS.ProcessEnv): Result<Config, ConfigError> {\n const parsed = RawSchema.safeParse(env);\n if (!parsed.success) {\n return err({ kind: \"invalid\", issues: parsed.error.flatten().fieldErrors });\n }\n const raw = parsed.data;\n const transport = raw.KAMINARI_AD_TRANSPORT;\n const logFormat = raw.KAMINARI_AD_LOG_FORMAT ?? (transport === \"stdio\" ? \"pretty\" : \"json\");\n const oauthScopesRaw = raw.KAMINARI_AD_OAUTH_SCOPES;\n const oauthScopes: readonly string[] =\n oauthScopesRaw === undefined\n ? DEFAULT_OAUTH_SCOPES\n : Object.freeze(oauthScopesRaw.split(/\\s+/).filter((s) => s.length > 0));\n return ok({\n transport,\n apiBaseUrl: raw.KAMINARI_AD_API_URL,\n logLevel: raw.KAMINARI_AD_LOG_LEVEL,\n logFormat,\n httpPort: raw.KAMINARI_AD_HTTP_PORT,\n sessionTtlSec: raw.KAMINARI_AD_SESSION_TTL_SEC,\n rateLimitRpm: raw.KAMINARI_AD_RATE_LIMIT_RPM,\n stdioApiKey: raw.KAMINARI_AD_API_KEY,\n oauthProtectedResource: raw.KAMINARI_AD_OAUTH_PROTECTED_RESOURCE,\n oauthProtectedResourceMetadataUrl: raw.KAMINARI_AD_OAUTH_PROTECTED_RESOURCE_METADATA_URL,\n oauthAuthorizationServerUrl: raw.KAMINARI_AD_OAUTH_AUTHORIZATION_SERVER_URL,\n oauthScopes,\n });\n}\n\n/**\n * Errors from {@link loadConfig}.\n */\nexport interface ConfigError {\n readonly kind: \"invalid\";\n readonly issues: Readonly<Record<string, readonly string[] | undefined>>;\n}\n","/**\n * CLI entrypoint for `@kaminari-ad/mcp`.\n *\n * Dispatches to a transport-specific composition root (stdio or HTTP)\n * based on the `--transport` flag (or `TRANSPORT` env var, with stdio\n * as the default).\n *\n * Wire-up is intentionally minimal — every dependency is constructed\n * inside the chosen bootstrap, never here. This file just parses argv\n * and config, then hands off.\n *\n * Imports policy: the STATIC imports below MUST stay free of any\n * runtime dependency that requires Node >= 22.19 (undici, MCP SDK,\n * pino). Otherwise the Node-version preflight in `main()` runs too\n * late — those modules crash at import time on older Node. The\n * transport bootstraps pull undici / MCP SDK / pino via dynamic\n * `await import()` so the preflight gets to fire first.\n */\n\nimport process from \"node:process\";\n\nimport { checkNodeVersion } from \"./shared/check-node-version.js\";\nimport { loadConfig, type Transport } from \"./shared/config.js\";\nimport { NAME, VERSION } from \"./shared/version.js\";\n\nfunction parseTransportFlag(argv: readonly string[]): Transport | undefined {\n for (const arg of argv) {\n if (arg === \"--transport=stdio\" || arg === \"--transport=http\") {\n return arg === \"--transport=stdio\" ? \"stdio\" : \"http\";\n }\n if (arg.startsWith(\"--transport=\")) {\n const value = arg.slice(\"--transport=\".length);\n if (value === \"stdio\" || value === \"http\") return value;\n }\n }\n return undefined;\n}\n\nasync function main(): Promise<number> {\n const argv = process.argv.slice(2);\n\n // Informational flags handled BEFORE the Node preflight so users on\n // any Node can confirm the binary resolves and discover the help\n // text — the only paths that touch zero runtime deps.\n if (argv.includes(\"--version\") || argv.includes(\"-v\")) {\n process.stdout.write(`${NAME} ${VERSION}\\n`);\n return 0;\n }\n if (argv.includes(\"--help\") || argv.includes(\"-h\")) {\n process.stdout.write(\n [\n `${NAME} ${VERSION}`,\n ``,\n `Usage:`,\n ` kaminari-ad-mcp [--transport=stdio|http]`,\n ``,\n `Transports:`,\n ` stdio (default) Local install — auth via KAMINARI_AD_API_KEY env var.`,\n ` http Hosted multi-tenant server — auth via incoming Authorization header.`,\n ``,\n `Configuration is documented in .env.example.`,\n ``,\n ].join(\"\\n\")\n );\n return 0;\n }\n\n // Preflight: bail with a clean upgrade message BEFORE any dynamic\n // import pulls undici / MCP SDK / pino — all three require\n // Node 22.19+ and crash at import time on older Node. Without this\n // gate users see undici's cryptic\n // `webidl.util.markAsUncloneable is not a function` instead.\n const check = checkNodeVersion(process.versions.node);\n if (check.isErr()) {\n process.stderr.write(`${check.error}\\n`);\n return 2;\n }\n\n const cliTransport = parseTransportFlag(argv);\n const env: NodeJS.ProcessEnv = cliTransport\n ? { ...process.env, KAMINARI_AD_TRANSPORT: cliTransport }\n : process.env;\n\n const configResult = loadConfig(env);\n if (configResult.isErr()) {\n process.stderr.write(`Invalid configuration: ${JSON.stringify(configResult.error.issues)}\\n`);\n return 2;\n }\n const config = configResult.value;\n\n if (config.transport === \"stdio\") {\n const { bootstrapStdio } = await import(\"./presentation/stdio/stdio-bootstrap.js\");\n return bootstrapStdio(config);\n }\n const { bootstrapHttp } = await import(\"./presentation/http/http-bootstrap.js\");\n return bootstrapHttp(config);\n}\n\nmain().then(\n (code) => {\n process.exit(code);\n },\n (error: unknown) => {\n process.stderr.write(`Fatal: ${error instanceof Error ? error.message : String(error)}\\n`);\n process.exit(1);\n }\n);\n"]}