Skip to content

refactor!: intent-level tools on the Python SDK, instead of one tool per endpoint - #1

Merged
backtrack5r3 (tomflenner) merged 4 commits into
mainfrom
refactor/intent-tools
Oct 21, 2026
Merged

backtrack5r3 (tomflenner) merged 4 commits into
mainfrom
refactor/intent-tools

Conversation

@tomflenner

@tomflenner backtrack5r3 (tomflenner) commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Why

The server turned each of the API's 105 OpenAPI operations into a tool. An agent had to drive Kaiten in HTTP verbs: UUIDs in, full-replacement PUT bodies out, and no way to tell a write that changed something from one that did not.

What

31 intent-level tools in Kaiten's own vocabulary (customers, instances, plans, grants, usage, flags, releases), designed on the MCP v2 rules in the product vision (knowledges/kaiten_vision_en.md § Agent-Readiness):

Rule How
Tools express an intent, not a CRUD mirror e.g. kaiten_provision_instance resolves a plan to its current version, finds the zone, creates the customer if asked, sets the term. kaiten_get_customer is a 360 view in one call
Reads return slices Projections keyed by slug; usage already judged against its grant (used / limit / percent / state, soft-limit ceiling included)
Writes merge and report truthfully Only passed fields change. Every write answers created / updated / unchanged / recorded / rejected / preview / deleted, with changes read back from Kaiten after the write. A no-op writes nothing and says unchanged
Constrained, stable identifiers Slugs everywhere, validated before any call
Trace before change Writes report affected_instances; kaiten_delete previews the blast radius until confirm=true; a schema change is dry-run first
Blast radius = the principal's Each request carries the caller's own token; the server holds none. Optional read-only mode
Area Read Write
Orientation overview · search
Customers & instances get_customer · get_instance · list_instances save_customer · provision_instance · update_instance
Usage get_usage report_usage
Catalog & plans get_catalog save_entitlement · save_license · set_license_state
Feature flags get_flags · evaluate_flags · test_rule save_flag
Releases get_deployments create_release · deploy_release · save_deployment_zone
Access & audit get_access · get_audit_trail issue_token · revoke_token
Settings get_metadata_fields · get_connectors save_metadata_field · configure_connector
Deletion delete

No contract is duplicated here. Every call goes through the official SDK, kaitencloud, which is generated from the OpenAPI document. That covers its methods and models, its enums (which become the tools' input schemas: EntitlementType, LicenseType, InstanceStatus, Scope…), kaitencloud.usage for enforcement arithmetic, and kaitencloud.targeting for flag shapes. One shared client reads the caller's bearer token per request through the SDK's callable credential.

Transports and packaging. FastMCP 2.14 → 4.1. One kaiten-mcp entry point serves:

  • stdio, for local clients (uvx --from git+…, or docker run -i … stdio);
  • Streamable HTTP, stateless, at /mcp, for remote clients behind the gateway. This is the current spec's remote transport; SSE is deprecated.

There is also GET /health, Host/Origin protection, and an image that defaults to HTTP and is built on every PR and published to GHCR from main and tags. MCP.md has configurations for Claude Code, Claude Desktop, Cursor and VS Code.

Verified

  • uv run pytest: 23 offline tests. They cover the limit arithmetic, honest statuses (a no-op sends only a GET), error mapping, read-only mode, and that every tool declares its effect.
  • uv run pytest -m e2e: 5/5 against a live SaaS stack (saas-api task up + Demo Sandbox seed, ksh_ PAT). Every tool, through a real MCP client. It covers the full commercial lifecycle (entitlement → plan → customer → instance → metering incl. a refused report → new version with a soft limit → upgrade/renew → in-place edit with affected instances → archive/default → delete preview/refusal), flags (lint refusal, merge, unknown variant), releases/zones/tokens, and metadata dry-run. It cleans up after itself.
  • Remote path: the image runs on the stack's network as mcp, and is reached through Envoy at localhost:6100/mcp over Streamable HTTP with the PAT. Without a PAT it gets a 401 at the gateway. stdio works via uv run, docker run -i and --read-only (15 tools).

Breaking

  • Every tool name and input changed.
  • KAITEN_API_BASE_URL must be Kaiten's gateway, not the API container: /mcp forwards the raw ksh_ token, and only the gateway exchanges it. KAITEN_API_OPENAPI_URL is gone.

Found along the way (upstream)

Follow-up PRs, one per real finding:

Finding Fix Effect on this PR
PUT /feature-flags/{slug} without slug blanks the flag's slug: the flag becomes unreachable, and the next slug-less update 500s kaitencloud/kaiten#55 —
sdk-python's feature_flags.update never sends the slug, so it triggers the above kaitencloud/sdk-python#7 kaiten_save_flag sends the slug through the SDK's transport until #7 is released
sdk-python has no methods for feature-flags/targeting/{context,lint,test}; they were excluded as console-only tooling kaitencloud/sdk-python#8 (a proposal: it reverses that exclusion) kaiten_test_rule and the save-flag lint call them through the SDK's transport until #8 is released
GET /releases never fills components, which the schema declares kaitencloud/kaiten#56 views.releases() reads each release on its own until #56 ships
REST can't list archived metadata fields, so unarchive takes an id REST can't find kaitencloud/kaiten#57 kaiten_save_metadata_field can archive but not find an archived field until #57 (and an SDK bump)
Local CDC dies after any task down + task up: Debezium's offsets outlive the database (this is why the audit trail was empty here) kaitencloud/kaiten#58 kaiten_get_audit_trail returns events again on a fresh stack

Not bugs, after a closer look:

  • Licenses never return familySlug. It is write-only by design ("Never returned — see familyId"). Plans are resolved through license_families.list(), which is the intended route.
  • An instance can't be taken out of its zone. Huma treats null on an optional field as absent, on every endpoint. kaiten's update-instance comment, which says an explicit null "fails validation", is inaccurate. Clearing a zone would be a new capability (and probably a new event), so it's a product decision, not a fix.
  • OFREP evaluation isn't in the SDK. That's on purpose: it belongs to OpenFeature and the OFREP provider. kaiten_evaluate_flags keeps calling OFREP through the SDK's transport.

Also fixed on this branch: the default gateway URL is kaiten's own .env.example port, 6060, not 6000.

🤖 Generated with Claude Code

…per endpoint

The server turned each of the API's 105 OpenAPI operations into a tool, so an
agent drove Kaiten in HTTP verbs: UUIDs in, full replacement bodies out, and
no way to tell a write that changed something from one that did not.

It now exposes 31 tools, each an intent in Kaiten's own vocabulary, following
the MCP v2 rules of the product vision: intent-level tools, sliced reads,
merging writes that report truthfully (created / updated / unchanged /
rejected, with the changes read back from Kaiten), slugs everywhere, and a
trace before a change (affected instances, delete previews).

Every call goes through the official SDK (kaitencloud), generated from the
OpenAPI document: its methods, models, enums (the tools' input schemas), its
enforcement arithmetic and its flag builders. The repository declares no API
contract of its own. Each request still acts as its caller's own token.

Also: FastMCP 4.1, stdio and Streamable HTTP transports from one
`kaiten-mcp` entry point, a read-only mode, an image that defaults to HTTP
and publishes to GHCR, and an e2e suite that drives every tool against a
live Kaiten.

BREAKING CHANGE: every tool name and input changed, and
KAITEN_API_BASE_URL must now point at Kaiten's gateway, which accepts ksh_
tokens, rather than at the API container.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
kaiten's .env.example puts the gateway on 6060; 6000 was a port nothing
listens on by default.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The licensing and contribution pack the other public repositories ship
(sdk-python, sdk-go, cli), adapted to the MCP server:

- LICENSE is now the Apache-2.0 text apache.org publishes, byte for byte:
  the previous one reworded section 9 and replaced the appendix.
- NOTICE, DCO.md, CONTRIBUTING.md, SECURITY.md, CODEOWNERS
  (@kaitencloud/engineering), the pull request template and the DCO
  workflow, which runs from main.
- Every Python file opens with the SPDX notice; tests/test_headers.py
  fails on one that does not.
- The wheel declares Apache-2.0 and carries LICENSE and NOTICE
  (license-files, hatchling 1.27 or later), and so does the image, which
  installs that wheel.

The CI jobs are now named lint, test and build: the checks the
default-branch ruleset of the public repositories requires.

The documentation pointed at the SaaS stack's local gateway (port 6100),
which is not public. The client examples now target this repository's
compose.yml (localhost:8000/mcp) and Kaiten's own gateway
(localhost:6060/api), and the e2e suite runs against Kaiten's
`task quickstart` dataset.

Signed-off-by: Alexandre Bergere <alexandre.bergere@kaiten.sh>
Signed-off-by: tomflenner <flennertom@gmail.com>
@tomflenner

Copy link
Copy Markdown
Contributor Author

Integration update:

  • chore: prepare the repository for its public release #2 has been fast-forward merged into refactor/intent-tools, preserving Alexandre Bergere's author and DCO sign-off.
  • Review confirmed LICENSE is byte-for-byte Apache's official text, SPDX coverage, packaged LICENSE/NOTICE, and lint/test/build gating.
  • Fixed DCO verification gaps in a signed-off follow-up commit: merge commits must be signed off, PRs above the API's 250-commit limit fail closed, incomplete lists fail, and only Git's parsed trailer block counts. Added regression tests.
  • uv sync --locked, Ruff lint/format, and uv run pytest: clean, 56 offline tests passed.
  • uv build --wheel: successful; exact LICENSE and NOTICE included.
  • Kaiten task quickstart completed on localhost:6060 with the Kaiten Sushi Shop demo dataset. uv run pytest -m e2e: 5 passed, using a temporary read/write PAT, revoked afterwards. The documented persistent e2e-mcp service account remains.
  • No sign-offs were added on behalf of other contributors; historical unsigned commits are unchanged.

Local Docker verification is running. GitHub currently reports no new CI run for the pushed head; the existing CI results belong to the previous head.

@tomflenner

Copy link
Copy Markdown
Contributor Author

Final verification: local Docker build passed and the runtime image contains LICENSE and NOTICE. #1 has now been merged using Rebase and merge as requested. All 56 offline tests and 5 live e2e tests passed before merging. Historical author/co-author metadata and existing sign-offs were preserved; no other contributor's commits were signed on their behalf.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants