@kadoa/mcp 0.6.5 → 0.7.0-rc.1

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.
Files changed (3) hide show
  1. package/README.md +263 -199
  2. package/dist/index.js +3681 -49099
  3. package/package.json +11 -9
package/README.md CHANGED
@@ -2,87 +2,10 @@
2
2
 
3
3
  Use [Kadoa](https://kadoa.com) from ChatGPT, Claude.ai, Claude Code, Cursor, and other MCP clients.
4
4
 
5
- ## Authentication
6
-
7
- Public API is the only OAuth authority. This server is an MCP resource server:
8
- it publishes protected-resource metadata, verifies RS256 resource tokens against
9
- Public API JWKS, enforces the exact issuer and resource audience, and exchanges
10
- each resource token for a request-local Public API token using a dedicated service secret.
11
- It does not mount authorization, registration, token, refresh, or revocation
12
- routes and does not store end-user credentials.
13
-
14
- This runtime targets the separate `mcp-server-beta` deployment. Production MCP
15
- at `https://mcp.kadoa.com/mcp` remains on legacy authentication until a separate
16
- approved migration.
17
-
18
- ```sh
19
- MCP_AUTH_ISSUER=https://api.kadoa.com/v5/auth
20
- MCP_AUTH_JWKS_URL=https://api.kadoa.com/v5/auth/.well-known/jwks.json
21
- MCP_AUTH_AUDIENCE=https://beta.mcp.kadoa.com/mcp
22
- MCP_DOWNSTREAM_TOKEN_ENDPOINT=https://api.kadoa.com/v5/auth/mcp/downstream-token
23
- # Inject MCP_DOWNSTREAM_EXCHANGE_SECRET from a dedicated secret store.
24
- MCP_ALLOWED_ORIGIN_HOSTNAMES=beta.mcp.kadoa.com
25
- MCP_LEGACY_TRANSPORT_ENABLED=true
26
- ```
27
-
28
- `MCP_LEGACY_TRANSPORT_ENABLED` controls protocol-version compatibility only.
29
- When `true`, supported 2025 protocol requests use the same central verifier and
30
- downstream exchange as modern requests. It never selects another authentication
31
- implementation. Missing central configuration fails startup.
32
-
33
- The exchange secret must contain at least 32 characters without whitespace.
34
- Generate at least 32 random bytes, encode as hex, and inject the same value into
35
- Public API as `KADOA_MCP_DOWNSTREAM_EXCHANGE_SECRET`. Never reuse the worker or
36
- legacy registry credentials. Secret rotation requires restarting both consumers.
37
- The service secret does not replace the user token or its authorization checks.
38
- No Google service account or metadata-server access is required.
39
-
40
- ### Historical local central-mode fixture
41
-
42
- This fixture depends on the earlier backend local-issuer configuration, which
43
- is absent from the narrowed beta rollout candidate. The commands below are not
44
- a validated proof for that candidate. Do not restore browser OAuth or local-issuer
45
- support to run this rollout.
46
-
47
- The normal `dev` entrypoint authenticates downstream exchange with
48
- `MCP_DOWNSTREAM_EXCHANGE_SECRET`. The test-only launcher imports the backend
49
- exchange owner and uses a local-only fixture secret. It requires
50
- `KADOA_ENVIRONMENT=local`, binds to `127.0.0.1:3002`, and is not included in the
51
- published package build.
52
-
53
- Start the backend services without the regular MCP child. The shared environment
54
- pins the backend issuer, Assistant MCP URL, resource audience, and downstream
55
- Public API audience to literal loopback addresses. If the backend root `.env`
56
- defines `KADOA_MCP_TOKEN_ALLOWED_RESOURCES`, set it to
57
- `http://127.0.0.1:3002/mcp` as well. Backend package processes load that file and
58
- a conflicting value makes the Assistant exchange fail with `invalid_target`.
59
- The backend `turbo.json` dev task must propagate every variable exported by
60
- `local-central-env.sh`; the launcher checks this before it starts.
61
-
62
- ```sh
63
- cd /home/gabrielziegler/kadoa/kadoa-backend
64
- direnv exec . bash -lc '
65
- source /home/gabrielziegler/kadoa/kadoa-mcp/tests/integration/local-central-env.sh
66
- ./start-kadoa.sh --quick --services=dashboard,api,shelly,events-gateway,operations
67
- '
68
- ```
69
-
70
- In a second terminal, start the local central resource server with the backend's
71
- local environment:
72
-
73
- ```sh
74
- direnv exec /home/gabrielziegler/kadoa/kadoa-backend bash -lc '
75
- source /home/gabrielziegler/kadoa/kadoa-mcp/tests/integration/local-central-env.sh
76
- cd /home/gabrielziegler/kadoa/kadoa-mcp
77
- KADOA_BACKEND_ROOT=/home/gabrielziegler/kadoa/kadoa-backend bun run dev:central-local
78
- '
79
- ```
80
-
81
- Open a new Dashboard Assistant session and request a workflow list. The proof
82
- covers the central resource server, backend-owned token exchange, live team
83
- membership checks, downstream Public API credentials, and real tool calls. It
84
- does not prove the current beta candidate, browser OAuth consent, or a deployed
85
- network boundary.
5
+ This repo builds the Kadoa MCP server: an HTTP-only [MCP](https://modelcontextprotocol.io)
6
+ resource server backed by Public API central authentication. There is no stdio
7
+ transport and no CLI entry point — see [Deployment & auth](#deployment--auth)
8
+ for what that means in practice.
86
9
 
87
10
  ## Remote Server (no install needed)
88
11
 
@@ -143,32 +66,108 @@ Point your client to `https://mcp.kadoa.com/mcp` with OAuth authentication.
143
66
 
144
67
  | Tool | Description |
145
68
  |------|-------------|
146
- | `scrape` | Immediately fetch one URL as markdown or raw HTML (shown only for enabled workspaces) |
147
- | `create_workflow` | Create an agentic navigation workflow from a prompt |
148
- | `create_realtime_monitor` | Create an asynchronous realtime monitoring workflow after persisting notification channels; returns workflow/session/thread/job IDs |
149
- | `list_workflows` | List workflows with status, Assistant session availability, template version drift, realtime health, and `awaitingUserInput`; filter with `statusFilters: ["group:attention"]` for everything waiting on you |
69
+ | `scrape` | Immediately fetch one URL as markdown or raw HTML (shown only for scrape-enabled workspaces) |
70
+
71
+ ### Workflows
72
+
73
+ | Tool | Description |
74
+ |------|-------------|
75
+ | `create_workflow` | Create a one-time or scheduled agentic-navigation extraction workflow, optionally from a template |
150
76
  | `get_workflow` | Get canonical intent, Assistant/session, template ownership, run health, schedule, location, monitoring, and validation details |
151
- | `request_workflow_update` | Ask the workflow Assistant to change extraction intent, navigation, sourcing, pagination, or generated behavior without changing the workflow ID |
77
+ | `update_workflow` | Update deterministic configuration such as name, URLs, schema, schedule timezone, monitoring, and unlimited rows |
78
+ | `run_workflow` | Run a workflow, including retrying a failed one-time or scheduled run |
79
+ | `delete_workflow` | Delete a workflow permanently |
80
+ | `approve_workflow` | Approve and activate a workflow that is in PREVIEW, PAUSED, or ERROR state |
81
+ | `pause_workflow` | Pause an ACTIVE workflow's schedule |
82
+ | `fetch_data` | Get a page of extracted data from a workflow (previews and small slices) |
83
+ | `export_data` | Get the full extracted dataset via a signed download URL |
84
+ | `list_workflows` | List workflows with status, Assistant session availability, template drift, realtime health, and `awaitingUserInput`; filter with `statusFilters: ["group:attention"]` for everything waiting on you |
85
+ | `list_workflow_runs` | List a workflow's execution run history (status, start/finish, record count, errors) |
86
+ | `get_workflow_history` | Get the workflow's configuration revision history (audit log) |
87
+
88
+ ### Realtime monitoring
89
+
90
+ | Tool | Description |
91
+ |------|-------------|
92
+ | `create_realtime_monitor` | Create an asynchronous realtime monitoring workflow after persisting notification channels |
93
+ | `list_changes` | List detected data changes across one or more realtime monitoring workflows |
94
+ | `get_change` | Get detailed information about one detected change, including the full data snapshot and diff |
95
+
96
+ ### Workflow Assistant
97
+
98
+ | Tool | Description |
99
+ |------|-------------|
152
100
  | `get_workflow_assistant` | Get the workflow Assistant's current activity and any pending clarification question |
153
- | `get_workflow_assistant_timeline` | Read paginated customer-visible Assistant messages and questions for one-time, scheduled, or realtime workflows |
101
+ | `get_workflow_assistant_timeline` | Read paginated customer-visible Assistant messages and questions |
154
102
  | `answer_workflow_assistant_question` | Answer the workflow Assistant's current clarification question and resume it |
103
+ | `request_workflow_update` | Ask the workflow Assistant to change extraction intent, navigation, sourcing, pagination, or generated behavior without changing the workflow ID |
155
104
  | `interrupt_workflow_assistant` | Safely interrupt active Assistant work without pausing the workflow schedule |
156
105
  | `resume_workflow_assistant` | Resume an idle/interrupted workflow Assistant with its persisted role and conversation |
157
106
  | `stop_workflow_assistant` | Stop active Assistant work without deleting or replacing the workflow |
158
107
  | `get_workflow_strategy` | Get the current customer-safe extraction/build strategy for an Assistant or custom-script workflow |
159
- | `get_workflow_history` | Get the workflow's configuration revision history (audit log) — who changed it, when, from which channel, and a `changedFields` summary per revision |
160
- | `list_activity` | List the team's Activity log (audit trail) across all workflows and resources: actor email, resource name, action, interface, timestamp, change summary; filter by time window, user, workflow, action, interface, resource type. Use `get_workflow_history` for one workflow's config diffs |
161
- | `list_workflow_runs` | List a workflow's execution run history (status, start/finish, record count, errors); filter by outcome and paginate |
108
+
109
+ ### Templates
110
+
111
+ | Tool | Description |
112
+ |------|-------------|
113
+ | `list_templates` | List all templates in the current team |
114
+ | `get_template` | Get a template by ID, including all published versions and schemas |
115
+ | `create_template` | Create a new (unpublished) template |
116
+ | `update_template` | Update a template's name and/or description only |
117
+ | `delete_template` | Delete (archive) a template |
118
+ | `create_template_version` | Publish a new immutable version of a template (prompt, schema, notifications) |
119
+ | `apply_template_version` | Push a template version onto its linked workflows |
120
+ | `save_workflow_as_template` | Create a template (or a new version of one) from an existing workflow's configuration |
121
+ | `list_template_schemas` | List schemas associated with a template |
122
+ | `list_template_workflows` | List the workflows linked to a template, flagging which are outdated |
123
+ | `link_template_to_workflows` | Link existing workflows to a template so they inherit and stay in sync with it |
124
+ | `unlink_template_from_workflows` | Unlink workflows from a template, keeping their current configuration |
125
+
126
+ ### Variables
127
+
128
+ | Tool | Description |
129
+ |------|-------------|
130
+ | `create_variable` | Create a variable (key/value pair referenced in prompts as `@key`) |
131
+ | `list_variables` | List all variables in the current team scope |
132
+ | `get_variable` | Get a specific variable by its ID |
133
+ | `update_variable` | Update an existing variable |
134
+ | `delete_variable` | Delete a variable permanently |
135
+
136
+ ### Notifications
137
+
138
+ | Tool | Description |
139
+ |------|-------------|
140
+ | `create_notification_channel` | Create a notification channel (email, webhook, Slack, or WebSocket), reusing an existing one for the same destination |
141
+ | `list_notification_channels` | List notification channels configured for a workflow or across the workspace |
142
+ | `delete_notification_channel` | Delete a notification channel by ID |
143
+ | `configure_notifications` | Create channels and event-to-channel settings for a workflow or workspace in one step |
144
+ | `list_notification_settings` | List notification settings (event-to-channel mappings) |
145
+ | `delete_notification_setting` | Delete a notification setting (the event-to-channel mapping, not the channel) |
146
+
147
+ ### Observability & activity
148
+
149
+ | Tool | Description |
150
+ |------|-------------|
162
151
  | `get_observability` | Team-wide workflow health for a window of days: uptime, failed runs, MTTR, records delivered, health counts, and a ranked list of down or degraded workflows (the Observability page data) |
163
- | `run_workflow` | Execute a workflow |
164
- | `fetch_data` | Get extracted data from a workflow |
165
- | `delete_workflow` | Delete a workflow |
166
- | `approve_workflow` | Approve and activate a workflow |
167
- | `pause_workflow` | Pause an active workflow |
168
- | `update_workflow` | Update deterministic configuration, including location, schedule timezone, monitoring conditions, and unlimited rows |
152
+ | `list_activity` | List the team's Activity log (audit trail) across all workflows and resources: actor email, resource name, action, interface, timestamp, change summary |
153
+
154
+ ### Support
155
+
156
+ | Tool | Description |
157
+ |------|-------------|
158
+ | `create_support_ticket` | File a support ticket for one workflow so the Kadoa team investigates it |
159
+
160
+ ### Account & team
161
+
162
+ | Tool | Description |
163
+ |------|-------------|
169
164
  | `whoami` | Show current user details, auth method, and team memberships |
170
165
  | `team_list` | List all teams you belong to and see which is active |
171
- | `team_switch` | Switch the active team by name or ID |
166
+
167
+ Team switching (a team_switch tool) exists in the code but is currently
168
+ disabled and hidden from every client's tool list — a credential is fixed to
169
+ one team for its lifetime under central auth; reconnect to choose a different
170
+ team.
172
171
 
173
172
  ## Workflow controls and behavior
174
173
 
@@ -350,130 +349,189 @@ delete_workflow for each, confirming before proceeding.
350
349
  - Restart your MCP client
351
350
  - Re-authenticate via OAuth if prompted
352
351
 
353
- ## Releases
354
-
355
- This repo ships two surfaces, and a "release" usually touches both:
356
-
357
- - **npm package `@kadoa/mcp`** — stdio CLI users install locally (`npx @kadoa/mcp`). Cut by [Release Please](https://github.com/googleapis/release-please) from Conventional Commits on `main`.
358
- - **Hosted server at `mcp.kadoa.com`** — Docker container on GKE, built and deployed from the [`kadoa-backend`](https://github.com/kadoa-org/kadoa-backend) monorepo, which pins a specific `@kadoa/mcp` version.
352
+ ## Deployment & auth
359
353
 
360
- Relevant files in `kadoa-backend`:
361
- - `infra/docker/mcp/Dockerfile.mcp-server` — image definition
362
- - `infra/docker/mcp/package.json` — pinned `@kadoa/mcp` version
363
- - `infra/cdk8s/mcp/charts/Server.ts` — k8s manifest source (prod chart only; the test deployment is provisioned ad hoc — see below)
354
+ This server is an MCP **resource server**, not an OAuth authorization server:
355
+ Public API is the only OAuth authority. This code publishes protected-resource
356
+ metadata, verifies RS256 resource tokens against Public API's JWKS, enforces
357
+ the exact issuer and resource audience, and exchanges each resource token for
358
+ a request-local Public API token using a dedicated service secret
359
+ (`src/kadoa-access-token.ts`, `src/downstream-token.ts`). It never mounts
360
+ authorization, registration, token, refresh, or revocation routes and never
361
+ stores end-user credentials. Old opaque MCP bearer credentials are rejected —
362
+ this build supports **central auth only**.
364
363
 
365
- ### Production release
364
+ There is no stdio transport (`src/http.ts` is the only server entry point) and
365
+ no `bin` in `package.json`, so `npx @kadoa/mcp` does not start a usable server.
366
366
 
367
- 1. Merge PRs to `main` using Conventional Commits (`feat:`, `fix:`, etc.). Release Please opens/maintains a `chore(main): release mcp x.y.z` PR.
368
- 2. Merge the release PR. The [`release-please.yml`](.github/workflows/release-please.yml) workflow tags, drafts a GitHub Release, and publishes to npm through GitHub Actions OIDC trusted publishing (`latest` dist-tag).
369
- 3. After npm publication, `release-please.yml` opens or updates a deterministic PR in `kadoa-backend` that bumps `infra/docker/mcp/package.json` and refreshes `bun.lock`. The PR validates a frozen production install and Docker image build, and remains manually mergeable.
370
- 4. Merge the backend PR to `main`. CI (`main-build-deploy.yml`) builds and pushes `europe-west3-docker.pkg.dev/oceanic-base-310208/kadoa-artifacts/mcp-server:<IMAGE_TAG>` (tag shown in the build summary).
371
- 5. Trigger the **Deploy to Production** workflow ([`deploy-prod.yml`](https://github.com/kadoa-org/kadoa-backend/actions/workflows/deploy-prod.yml)) with:
372
- - **Target cluster:** `gcp`
373
- - **Deployment scope:** `mcp`
374
- - **Image tag:** the tag from step 4
375
- - **Method:** `kubectl`
367
+ ### Where this code runs
376
368
 
377
- The cross-repository bump requires the `BACKEND_REPO_TOKEN` secret in this repository. The selected authentication approach is a fine-grained PAT scoped only to `kadoa-org/kadoa-backend` with Contents read/write and Pull requests read/write permissions (Metadata read is automatic). If the secret is not provisioned, the release workflow skips the backend bump with a warning rather than failing npm releases; an administrator must provision it before relying on automatic hosted-server updates.
369
+ Two deployments exist in `kadoa-backend` (see `infra/cdk8s/clusters/kadprod1.ts`
370
+ and `infra/cdk8s/staging/services.ts`):
378
371
 
379
- ### RC / test release
372
+ - **`https://beta.mcp.kadoa.com/mcp`** — runs the image built from this repo,
373
+ with central auth (`auth.mode: "central"`). It has carried all production
374
+ Dashboard Assistant traffic since 2026-09-23.
375
+ - **`https://mcp.kadoa.com/mcp`** (and staging) — still run a pinned legacy
376
+ image (`LEGACY_MCP_IMAGE_DIGEST` in `infra/cdk8s/mcp/images.ts`) built from
377
+ older code, with `auth.mode: "legacy"`. Promoting these to this repo's code
378
+ and central auth is a separate, approved migration.
380
379
 
381
- Use this when you want to validate a change end-to-end against real clients (Claude Desktop, Cursor, ChatGPT) before promoting to `latest` / prod. The flow mirrors the prod one, but every step targets `rc` channels.
380
+ The `@kadoa/mcp` npm package (this repo's build output plus `README.md`, per
381
+ `package.json` `files`) is not distributed as an end-user CLI — it is the
382
+ server bundle the backend image installs (`infra/docker/mcp/package.json`
383
+ pins the version).
382
384
 
383
- There are two ways to consume an RC:
385
+ ## Configuration
384
386
 
385
- - **Local stdio**: `npx @kadoa/mcp@rc` — installs from the `rc` dist-tag on npm. Good for quick sanity checks where the bug doesn't depend on the hosted OAuth flow.
386
- - **Hosted `mcp-server-test`**: a separate k8s deployment alongside prod in the same GKE cluster. Use it to validate protected-resource discovery, OAuth callbacks owned by Public API, and multi-tenant behavior.
387
+ All auth and downstream-exchange settings below are required for the server
388
+ to start (`bun run dev` fails immediately without them); there is no fallback
389
+ auth mode.
387
390
 
388
- #### Publish an RC to npm
391
+ | Variable | Description |
392
+ |---|---|
393
+ | `PORT` | HTTP port to listen on (default `3000`) |
394
+ | `MCP_SERVER_URL` | This deployment's own base URL; used only to derive a default for `MCP_ALLOWED_ORIGIN_HOSTNAMES` when that variable is unset and the hostname is a loopback address (default `http://localhost:<PORT>`) |
395
+ | `KADOA_PUBLIC_API_URI` | Public API base URL the server calls through `@kadoa/node-sdk` (default `https://api.kadoa.com`) |
396
+ | `KADOA_DASHBOARD_URL` | Base URL used to build `dashboardUrl` links in tool responses (default `https://www.kadoa.com`) |
397
+ | `MCP_AUTH_ISSUER` | Exact Public API OAuth issuer, e.g. `https://api.kadoa.com/v5/auth` |
398
+ | `MCP_AUTH_JWKS_URL` | JWKS URL for the issuer |
399
+ | `MCP_AUTH_AUDIENCE` | Exact MCP resource URL served by this deployment; must be HTTPS (loopback HTTP allowed) and its path must be `/mcp` |
400
+ | `MCP_DOWNSTREAM_TOKEN_ENDPOINT` | Public API's service-authenticated downstream exchange endpoint |
401
+ | `MCP_DOWNSTREAM_EXCHANGE_SECRET` | Dedicated service credential shared only with the downstream exchange; must be at least 32 characters with no whitespace, and must match Public API's `KADOA_MCP_DOWNSTREAM_EXCHANGE_SECRET` |
402
+ | `MCP_ALLOWED_ORIGIN_HOSTNAMES` | Comma-separated hostnames admitted when an HTTP `Origin` header is present; required outside loopback |
403
+ | `MCP_ALLOWED_HOSTNAMES` | Comma-separated `Host` header allow-list for DNS-rebinding protection (opt-in); loopback and this pod's own interface addresses are always added |
404
+ | `MCP_LEGACY_TRANSPORT_ENABLED` | `true` (default) serves supported 2025 MCP protocol versions through the same central verifier and downstream exchange as modern requests; `false` rejects them. It never selects a different authentication implementation |
405
+ | `LOG_LEVEL` | `debug`, `info` (default), `warn` or `error`; filters the structured JSON log lines written to stdout |
406
+ | `MCP_METRICS_PORT` | Port for a separate Prometheus `GET /metrics` listener (unset disables it). The Kadoa deployment uses `35536`, the port its pod scrape annotations point at; it is not exposed through the Service or ingress |
407
+
408
+ **Request logs and metrics.** Every `/mcp` request writes one JSON line to stdout
409
+ (`event: "mcp.request"`) with `requestId`, `era` (`modern`, `legacy`, `unknown`),
410
+ `httpMethod`, `method`, `toolName`, `clientInfo.name`, `protocolVersion`, `clientId`,
411
+ `userId`, `teamId`, `status`, `latencyMs`, `isError`, `rpcErrorCode`, `authReason`,
412
+ `traceId` and `aborted`. `isError` is true when the HTTP status is >= 400, the
413
+ outgoing JSON-RPC message carries an `error` object, or a tool result sets
414
+ `isError: true`; `rpcErrorCode` is the JSON-RPC error's `code` when the response
415
+ was a JSON-RPC error. `authReason` mirrors the reason class of a bearer-auth
416
+ rejection (see below) on that same request's line. `traceId` is the 32-hex-char
417
+ trace id from an incoming W3C `traceparent` (header, or the SEP-414 `_meta` trace
418
+ context if the request body carries one — that takes precedence), when present.
419
+ `aborted` is `true` only when the connection closed before the response finished
420
+ writing; it is omitted otherwise. Bearer-auth failures additionally write a
421
+ separate `auth.rejected` line with a reason class only. Tokens, request bodies and
422
+ tool arguments are never logged. The request id comes from a well-formed incoming
423
+ `x-request-id` (otherwise it is generated) and is returned in the response
424
+ header. `MCP_METRICS_PORT` exposes `mcp_http_requests_total{era,method,status}`
425
+ and `mcp_auth_rejections_total{reason}`; it must be a different port from `PORT`,
426
+ or the server refuses to start.
427
+
428
+ **Turning off legacy transport.** Set `MCP_LEGACY_TRANSPORT_ENABLED=false` only when,
429
+ over a trailing 14 days: the legacy share of successful requests,
430
+ `sum(increase(mcp_http_requests_total{era="legacy",status=~"2.."}[14d])) / sum(increase(mcp_http_requests_total{status=~"2.."}[14d]))`,
431
+ is below 1%; no first-party or top-10 client still appears in
432
+ `jsonPayload.event="mcp.request" AND jsonPayload.era="legacy"` grouped by
433
+ `jsonPayload.clientInfo.name` (legacy clients send it on `initialize`); and delegated
434
+ Dashboard Assistant traffic (`clientId` `kadoa-delegated:shelly-worker`) is modern
435
+ only. After switching, watch `sum(increase(mcp_http_requests_total{era="legacy"}[1d]))`
436
+ overall for a week, not only `status="400"`: with legacy transport off, a legacy
437
+ notification (no JSON-RPC response expected, e.g. `notifications/initialized`) is
438
+ answered `202 Accepted` rather than rejected, so most straggling legacy traffic shows
439
+ up as `era="legacy",status="202"`, and only a legacy request that expects a reply
440
+ (e.g. `initialize`) gets a `4xx` rejection. Setting the flag back to `true` restores
441
+ legacy serving.
442
+
443
+ Secret rotation requires restarting both this server and Public API. The
444
+ service secret does not replace the user's own token or its authorization
445
+ checks. No Google service account or metadata-server access is required.
389
446
 
390
- Manual — Release Please only cuts stable versions today.
447
+ ## Development
391
448
 
392
449
  ```bash
393
- # from a branch on kadoa-mcp
394
- bun install && bun run build
395
- # bump version to a prerelease, e.g. 0.5.7-rc.1
396
- npm version 0.5.7-rc.1 --no-git-tag-version
397
- npm publish --tag rc --access public
450
+ bun install
451
+ bun run dev # Run locally (requires all vars above to be set)
452
+ bun run dev:central-local # Recommended local workflow (see below)
453
+ bun run test # Run every discovered test (BUN_TEST=1 bun test)
454
+ bun run test:unit # Run the curated unit suite used in CI
455
+ bun run check-types # tsc --noEmit
456
+ bun run lint # bunx biome check
457
+ bun run build # Bundle for distribution
458
+ bun run smoke:dist # Boots dist/index.js and checks /health (lands with KAD-24989)
398
459
  ```
399
460
 
400
- Verify: `npm view @kadoa/mcp dist-tags`. The `rc` tag should now point to your version. Existing tags today: `latest`, `rc`, `next`.
401
-
402
- Promote later with: `npm dist-tag add @kadoa/mcp@0.5.7 latest` (run **after** the matching stable version has been published the normal way).
461
+ ### Local development (`dev:central-local`)
403
462
 
404
- #### Deploy the RC to `mcp-server-test`
463
+ `bun run dev` boots the same central-auth code path as production, so it
464
+ requires every `MCP_AUTH_*` / `MCP_DOWNSTREAM_*` variable above to already be
465
+ set and fails startup otherwise. `bun run dev:central-local`
466
+ (`tests/integration/local-central-server.ts`) wires that configuration up for
467
+ you against a **local** Kadoa backend instead of hand-setting each variable:
405
468
 
406
- 1. In `kadoa-backend`, set `infra/docker/mcp/package.json` `@kadoa/mcp` to the RC version, `bun install`, push to a branch and merge to `main`. CI builds the `mcp-server:<IMAGE_TAG>` image as usual.
407
- 2. Update the test deployment to the new image:
469
+ 1. Start the backend with `KADOA_ENVIRONMENT=local` so its local Public API
470
+ (`public-api`) listens on `http://127.0.0.1:12380`; source
471
+ `tests/integration/local-central-env.sh` in that shell first, and make sure
472
+ the backend's `turbo.json` `dev` task propagates the variables it sets.
473
+ 2. From this repo, with `KADOA_BACKEND_ROOT` pointing at that backend
474
+ checkout and `KADOA_ENVIRONMENT=local`:
408
475
 
409
476
  ```bash
410
- gcloud container clusters get-credentials kadoa-prod \
411
- --region=europe-west3 --project=oceanic-base-310208
412
-
413
- kubectl set image deployment/mcp-server-test \
414
- mcp-server=europe-west3-docker.pkg.dev/oceanic-base-310208/kadoa-artifacts/mcp-server:<IMAGE_TAG>
477
+ KADOA_BACKEND_ROOT=/path/to/kadoa-backend KADOA_ENVIRONMENT=local \
478
+ bun run dev:central-local
415
479
  ```
416
480
 
417
- 3. Port-forward to reach it (the test deployment has **no public ingress** by design):
481
+ 3. The server listens on `http://127.0.0.1:3002/mcp`, using a local-only
482
+ exchange secret and the backend's own token-exchange and team-membership
483
+ code (imported directly from `KADOA_BACKEND_ROOT`). It refuses to start if
484
+ the backend checkout, its `turbo.json` env propagation, or the local Public
485
+ API at `12380` aren't in place.
486
+
487
+ See `tests/integration/README.md` for the deeper integration proof this local
488
+ flow builds on.
489
+
490
+ ### Build
491
+
492
+ `bun run build` bundles `src/index.ts` to `dist/index.js` with all
493
+ dependencies kept external (`--packages=external`), so `dist/` resolves
494
+ packages from `node_modules` at run time instead of bundling them in. That is
495
+ what lets the backend image's `overrides` in `infra/docker/mcp/package.json`
496
+ (for example pinning `@kadoa/node-sdk`) take effect for the built server.
497
+ That override must be kept in the SDK range this package depends on; the
498
+ automated backend PR that bumps `@kadoa/mcp` after a release only bumps that
499
+ dependency, not the `overrides` block, so a mismatch there has to be fixed by
500
+ hand.
501
+ `bun run smoke:dist` boots the built `dist/index.js` and checks `/health` as
502
+ part of verifying a build.
418
503
 
419
- ```bash
420
- kubectl port-forward svc/mcp-server-test 3000:3000
421
- ```
422
-
423
- 4. Point your MCP client at `http://localhost:3000/mcp` and exercise the change.
424
-
425
- **Auth gotcha:** `auth.kadoa.com` enforces an `ALLOWED_CALLBACK_ORIGINS` allowlist (`app.kadoa.com`, `mcp.kadoa.com`, `kadoa.com`) plus a loopback bypass for `127.0.0.1` when `NODE_ENV !== production`. `localhost` is **not** treated the same as `127.0.0.1` by that check. The test pod is configured with `MCP_SERVER_URL=http://localhost:3000` so the port-forwarded session resolves correctly. If you change the local port, update the deployment env var too:
426
-
427
- ```bash
428
- kubectl set env deployment/mcp-server-test MCP_SERVER_URL=http://localhost:<PORT>
429
- ```
430
-
431
- #### Promotion checklist
432
-
433
- Before flipping `latest` / deploying to prod:
434
-
435
- - [ ] RC consumed locally (`npx @kadoa/mcp@rc`) — stdio tools work
436
- - [ ] RC consumed via `mcp-server-test` — OAuth login succeeds end-to-end
437
- - [ ] No new errors in the test pod's logs (`kubectl logs deployment/mcp-server-test -f`)
438
- - [ ] Release Please PR open with the stable version
439
-
440
- ## Development
441
-
442
- ```bash
443
- bun install
444
- bun run dev # Run locally
445
- bun run test # Run tests
446
- bun run build # Build for distribution
447
- ```
504
+ ## Releases
448
505
 
449
- ### Connecting to local services
506
+ This repo ships two surfaces, and a "release" usually touches both:
450
507
 
451
- To develop and test against a local Kadoa backend (instead of the production API), point the MCP at your local `public-api` service using the `KADOA_PUBLIC_API_URI` environment variable.
508
+ - **npm package `@kadoa/mcp`** — the server bundle the hosted deployments
509
+ install. Cut by [Release Please](https://github.com/googleapis/release-please) from Conventional Commits on `main`.
510
+ - **Hosted server at `beta.mcp.kadoa.com`** (production traffic) and
511
+ `mcp.kadoa.com` (legacy, separate migration) — Docker containers on GKE,
512
+ built and deployed from the [`kadoa-backend`](https://github.com/kadoa-org/kadoa-backend) monorepo, which pins a specific `@kadoa/mcp` version for the non-legacy deployment.
452
513
 
453
- **Prerequisites:** the `public-api` service must be running locally (default port `12380`).
514
+ Relevant files in `kadoa-backend`:
515
+ - `infra/docker/mcp/Dockerfile.mcp-server` — image definition
516
+ - `infra/docker/mcp/package.json` — pinned `@kadoa/mcp` version and dependency `overrides`
517
+ - `infra/cdk8s/clusters/kadprod1.ts` — k8s manifest source for `mcp-server` (legacy) and `mcp-server-beta` (this repo's code, central auth)
454
518
 
455
- **Run the MCP server locally:**
519
+ ### Production release
456
520
 
457
- ```bash
458
- KADOA_PUBLIC_API_URI=http://localhost:12380 bun run dev
459
- ```
521
+ 1. Merge PRs to `main` using Conventional Commits (`feat:`, `fix:`, etc.). Release Please opens/maintains a `chore(main): release mcp x.y.z` PR.
522
+ 2. Merge the release PR. The [`release-please.yml`](.github/workflows/release-please.yml) workflow tags, drafts a GitHub Release, and publishes to npm through GitHub Actions OIDC trusted publishing (`latest` dist-tag).
523
+ 3. After npm publication, the same workflow opens or updates a deterministic PR in `kadoa-backend` that bumps `infra/docker/mcp/package.json` and refreshes its lockfile, validating a production install and Docker image build. It remains manually mergeable, and is skipped with a warning (not a failure) if the `BACKEND_REPO_TOKEN` secret isn't provisioned.
524
+ 4. Merge the backend PR to `main`. CI builds and pushes the `mcp-server` image.
525
+ 5. Deploy the new image tag to the target deployment (`mcp-server-beta` for this repo's code) through the backend's deploy workflow.
460
526
 
461
- The server starts in HTTP mode. You authenticate via OAuth the same way as with the remote server.
527
+ ### RC / test release
462
528
 
463
- ### HTTP server environment variables
529
+ Use this to validate a change end-to-end before promoting to the `latest` npm dist-tag. Trigger the [`release-please.yml`](.github/workflows/release-please.yml) workflow manually (`workflow_dispatch`) with an `rc_version` like `0.6.0-rc.1`; it publishes that version under the npm `rc` dist-tag using the same trusted-publishing flow as a stable release.
464
530
 
465
- | Variable | Description |
466
- |---|---|
467
- | `MCP_AUTH_ISSUER` | Exact Public API OAuth issuer, such as `https://api.kadoa.com/v5/auth` |
468
- | `MCP_AUTH_JWKS_URL` | JWKS URL for the issuer |
469
- | `MCP_AUTH_AUDIENCE` | Exact MCP resource URL served by this deployment; its path must be `/mcp` |
470
- | `MCP_DOWNSTREAM_TOKEN_ENDPOINT` | Public API service-authenticated downstream exchange endpoint |
471
- | `MCP_DOWNSTREAM_EXCHANGE_SECRET` | Dedicated service credential shared only with the downstream exchange; must satisfy the same validation as Public API `KADOA_MCP_DOWNSTREAM_EXCHANGE_SECRET` |
472
- | `MCP_ALLOWED_ORIGIN_HOSTNAMES` | Comma-separated hostnames admitted when an HTTP Origin header is present; required outside loopback |
473
- | `MCP_LEGACY_TRANSPORT_ENABLED` | `true` serves supported 2025 protocol versions through central auth; `false` rejects them |
531
+ Verify: `npm view @kadoa/mcp dist-tags`. The `rc` tag should now point to your version.
474
532
 
475
- All authentication and downstream-exchange settings are required. Old opaque
476
- MCP bearer credentials are rejected.
533
+ Because there is no CLI entry point, consuming an RC means pointing a hosted
534
+ test deployment at the new image tag, not `npx`ing the package locally.
477
535
 
478
536
  ## Contributing
479
537
 
@@ -487,7 +545,7 @@ A typical feature touches three repos — **`kadoa-backend` → `kadoa-sdks` →
487
545
  | SDK low-level | `kadoa-sdks/sdks/node/src/generated/api/*` | Auto-generated axios client. Raw HTTP, typed from the OpenAPI spec. **Do not hand-edit.** |
488
546
  | SDK domain (high-level) | `kadoa-sdks/sdks/node/src/domains/<thing>/<thing>.service.ts` | Thin domain wrapper over the generated client. `client.<thing>.<action>(...)`. This is what callers use. |
489
547
  | CLI | `kadoa-cli/src/commands/<thing>.ts` | Commander action: flag parsing, spinner, table/json output. Calls SDK domain. |
490
- | MCP (this repo) | `kadoa-mcp/src/tools.ts` | Zod input schema + tool description tuned for LLMs. Calls SDK domain. |
548
+ | MCP (this repo) | `kadoa-mcp/src/tools/<domain>.ts` | Zod input schema + tool description tuned for LLMs. Calls SDK domain. |
491
549
 
492
550
  Mental model:
493
551
  - **SDK** = pure function over HTTP.
@@ -515,30 +573,36 @@ CLI and MCP both call **identical** SDK domain methods. A new feature added to t
515
573
 
516
574
  3. **MCP** (this repo)
517
575
  - Bump `@kadoa/node-sdk` in `package.json` to the new version, `bun install`.
518
- - Add the tool in `src/tools.ts`:
519
- - Zod input schema (use coercion helpers in `src/coercion.ts` where needed — LLM clients sometimes send strings for numbers/bools).
576
+ - Add the tool in the matching `src/tools/<domain>.ts` register function (keep `src/tools/index.ts` order in mind — it is the `tools/list` order):
577
+ - Zod input schema (coercion helpers for inputs LLM clients sometimes send as strings, e.g. numbers/bools, live in `src/tools/shared/coercion.ts`; shared shapes in `src/tools/shared/schemas.ts`).
520
578
  - Description string written **for the LLM**: when to call it, what inputs mean, what the response shape is.
521
579
  - Implementation calls `ctx.client.<thing>.<action>(...)` — never raw HTTP.
522
580
  - Add a unit test under `tests/unit/`.
581
+ - If the change intentionally alters a tool's name, title, description, schema or annotations, regenerate the snapshot: `UPDATE_TOOLS_SNAPSHOT=1 BUN_TEST=1 bun test tests/unit/tools-list-snapshot.test.ts`, then `./node_modules/.bin/biome format --write tests/unit/fixtures/tools-list.snapshot.json`.
523
582
  - Use a Conventional Commit message (`feat(mcp): ...`, `fix(mcp): ...`). Release Please will cut the next version on merge to `main`.
524
583
 
525
584
  4. **CLI** (`kadoa-cli`), if the feature is user-facing on the command line: mirror the MCP step in `src/commands/<thing>.ts`. Same SDK call, different UX.
526
585
 
527
586
  ### Anti-patterns
528
587
 
529
- - ❌ Raw `axios` calls or hand-rolled HTTP in `src/tools.ts`. Always go through `ctx.client.<thing>`.
588
+ - ❌ Raw `axios` calls or hand-rolled HTTP in `src/tools/`. Always go through `ctx.client.<thing>`.
530
589
  - ❌ Putting tool-shaped logic (input validation, response shaping) in the MCP layer when the SDK should expose it. If two clients would need the same logic, it belongs in the SDK.
531
590
  - ❌ Editing files under `sdks/node/src/generated/` by hand. They get overwritten by `kadoa-codegen generate`.
532
591
  - ❌ Bundling a spec update and an SDK feature in the same commit — keep `specs/` PRs separate from domain-method PRs for clean review.
533
592
 
534
593
  ### Where to find things in this repo
535
594
 
536
- - `src/tools.ts` — every MCP tool. Single file by design; search for the tool name.
537
- - `src/kadoa-access-token.ts` — verifies Public API resource tokens.
595
+ - `src/tools/` — MCP tools, one module per domain (`workflows`, `assistant`, `templates`, `notifications`, `variables`, `observability`, `team`, `scrape`, `support`); shared zod schemas, coercion, errors, results and dashboard URLs in `src/tools/shared/`. `src/tools/index.ts` fixes registration order.
596
+ - `src/server.ts` — assembles the `McpServer` (tool registration, capability gating, instructions); `src/index.ts` is the CLI entry.
597
+ - `src/http.ts` — Express resource server, MCP transport mounting, and graceful shutdown.
598
+ - `src/kadoa-access-token.ts` — verifies Public API resource tokens (central auth config, JWT verification).
538
599
  - `src/downstream-token.ts` — exchanges resource tokens for request-local Public API credentials.
539
600
  - `src/client.ts` — wires `@kadoa/node-sdk` per request.
540
- - `src/http.ts` — Express resource server and transport adapter.
541
- - `tests/unit/` — bun tests; run with `bun run test`.
601
+ - `src/principal.ts` — the `McpPrincipal` type carried through auth and tool context.
602
+ - `src/notification-channels.ts` — channel identity/reuse logic shared by the notification tools.
603
+ - `src/error-summary.ts` — redacts credentials from error messages before they're logged.
604
+ - `tests/unit/` — bun tests; run with `bun run test:unit` (curated) or `bun run test` (everything discovered).
605
+ - `tests/integration/` — local and CI-owned integration fixtures against a real backend; not part of `test:unit`.
542
606
 
543
607
  ## License
544
608