kaitencloud-mcp 0.2.0__tar.gz

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 (41) hide show
  1. kaitencloud_mcp-0.2.0/.github/CODEOWNERS +6 -0
  2. kaitencloud_mcp-0.2.0/.github/PULL_REQUEST_TEMPLATE.md +21 -0
  3. kaitencloud_mcp-0.2.0/.github/workflows/ci.yml +142 -0
  4. kaitencloud_mcp-0.2.0/.github/workflows/dco.yml +126 -0
  5. kaitencloud_mcp-0.2.0/.gitignore +4 -0
  6. kaitencloud_mcp-0.2.0/CHANGELOG.md +26 -0
  7. kaitencloud_mcp-0.2.0/CONTRIBUTING.md +101 -0
  8. kaitencloud_mcp-0.2.0/DCO.md +37 -0
  9. kaitencloud_mcp-0.2.0/Dockerfile +30 -0
  10. kaitencloud_mcp-0.2.0/LICENSE +202 -0
  11. kaitencloud_mcp-0.2.0/MCP.md +117 -0
  12. kaitencloud_mcp-0.2.0/NOTICE +3 -0
  13. kaitencloud_mcp-0.2.0/PKG-INFO +149 -0
  14. kaitencloud_mcp-0.2.0/README.md +129 -0
  15. kaitencloud_mcp-0.2.0/SECURITY.md +21 -0
  16. kaitencloud_mcp-0.2.0/compose.yml +17 -0
  17. kaitencloud_mcp-0.2.0/pyproject.toml +57 -0
  18. kaitencloud_mcp-0.2.0/src/kaiten_mcp/__init__.py +4 -0
  19. kaitencloud_mcp-0.2.0/src/kaiten_mcp/__main__.py +43 -0
  20. kaitencloud_mcp-0.2.0/src/kaiten_mcp/config.py +50 -0
  21. kaitencloud_mcp-0.2.0/src/kaiten_mcp/sdk.py +111 -0
  22. kaitencloud_mcp-0.2.0/src/kaiten_mcp/server.py +95 -0
  23. kaitencloud_mcp-0.2.0/src/kaiten_mcp/shapes.py +301 -0
  24. kaitencloud_mcp-0.2.0/src/kaiten_mcp/tools/__init__.py +41 -0
  25. kaitencloud_mcp-0.2.0/src/kaiten_mcp/tools/access.py +175 -0
  26. kaitencloud_mcp-0.2.0/src/kaiten_mcp/tools/accounts.py +414 -0
  27. kaitencloud_mcp-0.2.0/src/kaiten_mcp/tools/catalog.py +496 -0
  28. kaitencloud_mcp-0.2.0/src/kaiten_mcp/tools/delete.py +153 -0
  29. kaitencloud_mcp-0.2.0/src/kaiten_mcp/tools/flags.py +395 -0
  30. kaitencloud_mcp-0.2.0/src/kaiten_mcp/tools/overview.py +159 -0
  31. kaitencloud_mcp-0.2.0/src/kaiten_mcp/tools/releases.py +240 -0
  32. kaitencloud_mcp-0.2.0/src/kaiten_mcp/tools/settings.py +187 -0
  33. kaitencloud_mcp-0.2.0/src/kaiten_mcp/tools/usage.py +147 -0
  34. kaitencloud_mcp-0.2.0/src/kaiten_mcp/views.py +193 -0
  35. kaitencloud_mcp-0.2.0/tests/e2e/test_live.py +391 -0
  36. kaitencloud_mcp-0.2.0/tests/test_dco.py +73 -0
  37. kaitencloud_mcp-0.2.0/tests/test_headers.py +31 -0
  38. kaitencloud_mcp-0.2.0/tests/test_health.py +55 -0
  39. kaitencloud_mcp-0.2.0/tests/test_shapes.py +52 -0
  40. kaitencloud_mcp-0.2.0/tests/test_tools.py +104 -0
  41. kaitencloud_mcp-0.2.0/uv.lock +1525 -0
@@ -0,0 +1,6 @@
1
+ # Every change to this repository needs the approval of a member of @kaitencloud/engineering.
2
+ #
3
+ # The default-branch ruleset requires a review from a code owner. With this file, that means an
4
+ # approval from one of the team's members -- never from a GitHub App or a workflow's token,
5
+ # which cannot belong to a team, and never from the pull request's own author.
6
+ * @kaitencloud/engineering
@@ -0,0 +1,21 @@
1
+ ## What does this PR change?
2
+
3
+ <!-- Describe the change. -->
4
+
5
+ ## Why?
6
+
7
+ <!-- Explain the motivation. -->
8
+
9
+ ## Testing
10
+
11
+ <!-- Explain how this was tested. -->
12
+
13
+ ## Checklist
14
+
15
+ - [ ] I have read `CONTRIBUTING.md`.
16
+ - [ ] Every commit in this PR includes a valid DCO `Signed-off-by` line.
17
+ - [ ] I have the right to submit all material in this PR.
18
+ - [ ] I have not included secrets or confidential data.
19
+ - [ ] I have updated tests where appropriate.
20
+ - [ ] I have updated documentation where appropriate.
21
+ - [ ] I have preserved required third-party licenses and attributions.
@@ -0,0 +1,142 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: ["main"]
6
+ tags: ["v*"]
7
+ pull_request:
8
+ branches: ["main"]
9
+
10
+ permissions:
11
+ contents: read
12
+
13
+ # The job names are the checks the default-branch ruleset requires: lint, test and build.
14
+ jobs:
15
+ lint:
16
+ name: lint
17
+ runs-on: ubuntu-latest
18
+ steps:
19
+ - uses: actions/checkout@v5
20
+
21
+ - name: Set up uv
22
+ uses: astral-sh/setup-uv@v7
23
+ with:
24
+ version: "0.9.1"
25
+ enable-cache: true
26
+
27
+ - name: Install
28
+ run: uv sync --locked
29
+
30
+ - name: Lint
31
+ run: uv run ruff check . && uv run ruff format --check .
32
+
33
+ test:
34
+ name: test
35
+ runs-on: ubuntu-latest
36
+ steps:
37
+ - uses: actions/checkout@v5
38
+
39
+ - name: Set up uv
40
+ uses: astral-sh/setup-uv@v7
41
+ with:
42
+ version: "0.9.1"
43
+ enable-cache: true
44
+
45
+ - name: Install
46
+ run: uv sync --locked
47
+
48
+ # The e2e suite needs a live Kaiten and is deselected by default; these
49
+ # drive every tool's wiring through the real SDK over a mocked API.
50
+ - name: Test
51
+ run: uv run pytest
52
+
53
+ # A pull request builds the image so a broken Dockerfile fails here; main and
54
+ # tags publish it to GHCR as ghcr.io/kaitencloud/kaiten-mcp. The package is
55
+ # created by this repository's first publish and linked to it by the image's
56
+ # source label, so it takes this repository's visibility: publish only once the
57
+ # repository is public. (`mcp` is a different, older package, linked to
58
+ # kaiten-internal, that this repository's token cannot write.)
59
+ build:
60
+ name: build
61
+ runs-on: ubuntu-latest
62
+ needs: [lint, test]
63
+ permissions:
64
+ contents: read
65
+ packages: write
66
+ env:
67
+ IMAGE: ghcr.io/${{ github.repository_owner }}/kaiten-mcp
68
+ steps:
69
+ - uses: actions/checkout@v5
70
+
71
+ - uses: docker/setup-qemu-action@v3
72
+ - uses: docker/setup-buildx-action@v3
73
+
74
+ # Logged in on every run of this repository, pull requests included: the
75
+ # build cache lives in the registry and is read through this login. A pull
76
+ # request from a fork gets a read-only token, which still reads the cache.
77
+ - name: Log in to GHCR
78
+ uses: docker/login-action@v3
79
+ with:
80
+ registry: ghcr.io
81
+ username: ${{ github.actor }}
82
+ password: ${{ secrets.GITHUB_TOKEN }}
83
+
84
+ - id: meta
85
+ uses: docker/metadata-action@v5
86
+ with:
87
+ images: ${{ env.IMAGE }}
88
+ tags: |
89
+ type=ref,event=branch
90
+ type=ref,event=pr
91
+ type=semver,pattern={{version}}
92
+ type=semver,pattern={{major}}.{{minor}}
93
+ type=sha
94
+
95
+ # The cache is a tag of the image's own package, not the GitHub Actions cache:
96
+ # that one is read-only on re-runs of an old pull request, and failed this
97
+ # job's build with "failed to reserve cache". Only main and tags write it.
98
+ - name: Build
99
+ uses: docker/build-push-action@v6
100
+ with:
101
+ context: .
102
+ platforms: linux/amd64,linux/arm64
103
+ push: ${{ github.event_name == 'push' }}
104
+ tags: ${{ steps.meta.outputs.tags }}
105
+ labels: ${{ steps.meta.outputs.labels }}
106
+ cache-from: type=registry,ref=${{ env.IMAGE }}:buildcache
107
+ cache-to: ${{ github.event_name == 'push' && format('type=registry,ref={0}:buildcache,mode=max', env.IMAGE) || '' }}
108
+
109
+ # A `v*` tag publishes the Python package to PyPI as kaitencloud-mcp, once lint,
110
+ # test and the image build have passed on it. Trusted publishing: no token is
111
+ # stored; PyPI trusts this workflow, in the `pypi` environment, to publish the
112
+ # project (see CONTRIBUTING.md, "Releasing").
113
+ publish:
114
+ name: publish
115
+ runs-on: ubuntu-latest
116
+ needs: [lint, test, build]
117
+ if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
118
+ environment: pypi
119
+ permissions:
120
+ contents: read
121
+ id-token: write
122
+ steps:
123
+ - uses: actions/checkout@v5
124
+
125
+ - name: Set up uv
126
+ uses: astral-sh/setup-uv@v7
127
+ with:
128
+ version: "0.9.1"
129
+
130
+ - name: The tag is the project's version
131
+ run: |
132
+ version=$(uv version --short)
133
+ if [ "v$version" != "$GITHUB_REF_NAME" ]; then
134
+ echo "::error::tag $GITHUB_REF_NAME does not match pyproject.toml's version $version"
135
+ exit 1
136
+ fi
137
+
138
+ - name: Build
139
+ run: uv build
140
+
141
+ - name: Publish
142
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,126 @@
1
+ name: DCO
2
+
3
+ # Every commit of a pull request must be signed off by its author, as DCO.md and
4
+ # CONTRIBUTING.md ask: a `Signed-off-by: Name <email>` trailer naming the commit's author.
5
+ # Require the `DCO` check on main; this workflow is the source of truth.
6
+ #
7
+ # pull_request_target runs the workflow as it is on main, never as a pull request edits it,
8
+ # so a contributor cannot change the check that judges their own commits. It is safe because
9
+ # nothing here checks out or runs the pull request's code: the check below reads the commits'
10
+ # metadata from the API, and it needs no other file, so it can be copied to any repository.
11
+ on:
12
+ pull_request_target:
13
+ branches: ["main"]
14
+
15
+ permissions:
16
+ contents: read
17
+ pull-requests: read
18
+
19
+ jobs:
20
+ dco:
21
+ name: DCO
22
+ runs-on: ubuntu-latest
23
+ timeout-minutes: 5
24
+ steps:
25
+ - name: Every commit is signed off by its author
26
+ shell: python
27
+ env:
28
+ GITHUB_TOKEN: ${{ github.token }}
29
+ PR_NUMBER: ${{ github.event.pull_request.number }}
30
+ PR_COMMIT_COUNT: ${{ github.event.pull_request.commits }}
31
+ run: |
32
+ """Fail when a commit of the pull request is not signed off by its author (DCO 1.1)."""
33
+
34
+ import json
35
+ import os
36
+ import re
37
+ import subprocess
38
+ import sys
39
+ import urllib.request
40
+
41
+ SIGN_OFF = re.compile(
42
+ r"^Signed-off-by:[ \t]*(?P<name>.+?)[ \t]*<(?P<email>[^<>]+)>[ \t]*$", re.M
43
+ )
44
+ FIX = (
45
+ "Sign commits off with `git commit -s`. To fix this pull request:\n\n"
46
+ " git commit --amend --signoff --no-edit # the latest commit\n"
47
+ " git rebase --signoff origin/main # every commit of the branch\n"
48
+ " git push --force-with-lease\n\n"
49
+ "See CONTRIBUTING.md and DCO.md."
50
+ )
51
+
52
+
53
+ def problem(commit):
54
+ """Why a commit, as the API lists a pull request's commits, fails; None if it passes.
55
+
56
+ A commit passes when one of its Signed-off-by trailers names its author, by name and
57
+ by email, ignoring case. Merge commits can contain conflict resolutions and must be
58
+ signed off too. A bot account certifies nothing, so it is not checked.
59
+ """
60
+ if (commit.get("author") or {}).get("type") == "Bot":
61
+ return None
62
+ author = commit["commit"]["author"]
63
+ # Parse only the final trailer block, not quoted sign-offs in the message body.
64
+ trailers = subprocess.run(
65
+ ["git", "interpret-trailers", "--parse"],
66
+ input=commit["commit"]["message"], text=True, capture_output=True, check=True,
67
+ ).stdout
68
+ signoffs = [(m["name"], m["email"]) for m in SIGN_OFF.finditer(trailers)]
69
+ if any(
70
+ name.lower() == author["name"].lower() and email.lower() == author["email"].lower()
71
+ for name, email in signoffs
72
+ ):
73
+ return None
74
+ expected = f"{author['name']} <{author['email']}>"
75
+ if not signoffs:
76
+ return f"no Signed-off-by; expected {expected}"
77
+ found = ", ".join(f"{name} <{email}>" for name, email in signoffs)
78
+ return f"signed off by {found}, not by its author: expected {expected}"
79
+
80
+
81
+ def commits():
82
+ """Every commit of the pull request, page by page."""
83
+ api = os.environ.get("GITHUB_API_URL", "https://api.github.com")
84
+ repository, number = os.environ["GITHUB_REPOSITORY"], os.environ["PR_NUMBER"]
85
+ page = 1
86
+ while True:
87
+ request = urllib.request.Request(
88
+ f"{api}/repos/{repository}/pulls/{number}/commits?per_page=100&page={page}",
89
+ headers={
90
+ "Accept": "application/vnd.github+json",
91
+ "Authorization": f"Bearer {os.environ['GITHUB_TOKEN']}",
92
+ "X-GitHub-Api-Version": "2022-11-28",
93
+ },
94
+ )
95
+ with urllib.request.urlopen(request, timeout=30) as response:
96
+ batch = json.load(response)
97
+ yield from batch
98
+ if len(batch) < 100:
99
+ return
100
+ page += 1
101
+
102
+
103
+ def main():
104
+ # GitHub's pull-request commits endpoint is capped at 250, even with pagination.
105
+ expected = int(os.environ["PR_COMMIT_COUNT"])
106
+ if expected > 250:
107
+ print("::error title=DCO::Pull requests over 250 commits must be split for DCO verification.")
108
+ return 1
109
+ listed = list(commits())
110
+ if len(listed) != expected:
111
+ print("::error title=DCO::Commit count changed or the API returned an incomplete list; rerun the check.")
112
+ return 1
113
+ failed = [(commit, why) for commit in listed if (why := problem(commit)) is not None]
114
+ for commit, why in failed:
115
+ subject = commit["commit"]["message"].splitlines()[0]
116
+ print(f"::error title=DCO::{commit['sha']} {subject}: {why}")
117
+ if failed:
118
+ print(f"\n{len(failed)} of {len(listed)} commits are not signed off by their author.")
119
+ print(f"\n{FIX}")
120
+ return 1
121
+ print(f"All {len(listed)} commits are signed off by their author.")
122
+ return 0
123
+
124
+
125
+ if __name__ == "__main__":
126
+ sys.exit(main())
@@ -0,0 +1,4 @@
1
+ __pycache__/
2
+ *.pyc
3
+ *.pyo
4
+ .venv/
@@ -0,0 +1,26 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here. The format follows
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project adheres to
5
+ [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## [0.2.0] - 2026-10-22
8
+
9
+ The first public release of the Kaiten MCP server. It replaces the server that turned every operation
10
+ of the Kaiten API into a tool of its own, which was never released.
11
+
12
+ ### Added
13
+
14
+ - 31 intent-level tools over the Kaiten API, built on the official Python SDK (`kaitencloud`):
15
+ orientation and search, customers and instances, usage, the catalog and Licenses, feature flags,
16
+ releases and deployment zones, access and audit, settings, and a deletion that previews its blast
17
+ radius until confirmed.
18
+ - The stdio transport, with the token in `KAITEN_API_TOKEN`, and Streamable HTTP at `/mcp`, where
19
+ each request brings its own `Authorization: Bearer ksh_...` header.
20
+ - A read-only mode (`KAITEN_MCP_READ_ONLY=true` or `--read-only`) that registers the 15 read tools
21
+ only.
22
+ - `GET /health`: the server's version, and whether Kaiten's gateway answers.
23
+ - The package on PyPI, [`kaitencloud-mcp`](https://pypi.org/project/kaitencloud-mcp/), and the image
24
+ `ghcr.io/kaitencloud/kaiten-mcp`.
25
+
26
+ [0.2.0]: https://github.com/kaitencloud/mcp/releases/tag/v0.2.0
@@ -0,0 +1,101 @@
1
+ # Contributing to the Kaiten MCP server
2
+
3
+ Thank you for contributing to the Kaiten MCP server.
4
+
5
+ This project is licensed under the Apache License, Version 2.0.
6
+
7
+ ## Developer Certificate of Origin
8
+
9
+ Kaiten uses the Developer Certificate of Origin 1.1 (DCO) for contributions.
10
+
11
+ Every commit must include a `Signed-off-by` trailer matching the commit author.
12
+
13
+ Use:
14
+
15
+ ```bash
16
+ git commit -s -m "Describe your change"
17
+ ```
18
+
19
+ This produces:
20
+
21
+ ```text
22
+ Signed-off-by: Jane Doe <jane@example.com>
23
+ ```
24
+
25
+ `git commit -s` is a **DCO sign-off**: it certifies the contribution under
26
+ [DCO.md](./DCO.md). `git commit -S`, with a capital S, is a **cryptographic
27
+ commit signature** made with a GPG or SSH key. They are not the same thing, and
28
+ the sign-off is what this project requires. There is nothing to install.
29
+
30
+ ### Fixing a missing sign-off
31
+
32
+ A check named **DCO** verifies every commit of every pull request, and it is the
33
+ final word. If it fails, it names the commits to fix.
34
+
35
+ For the latest commit:
36
+
37
+ ```bash
38
+ git commit --amend --signoff --no-edit
39
+ git push --force-with-lease
40
+ ```
41
+
42
+ For several commits, sign off every commit of your branch at once:
43
+
44
+ ```bash
45
+ git rebase --signoff origin/main
46
+ git push --force-with-lease
47
+ ```
48
+
49
+ ## Before you open a pull request
50
+
51
+ ```bash
52
+ uv sync
53
+ uv run ruff check . && uv run ruff format --check .
54
+ uv run pytest
55
+ ```
56
+
57
+ Every Python file opens with the project's license notice, and the tests check it:
58
+
59
+ ```python
60
+ # Copyright 2026 KAITEN INC
61
+ # SPDX-License-Identifier: Apache-2.0
62
+ ```
63
+
64
+ ## Contribution rules
65
+
66
+ Please:
67
+
68
+ - keep pull requests focused;
69
+ - add or update tests when behavior changes;
70
+ - update documentation when relevant;
71
+ - do not include secrets, customer data, or confidential information;
72
+ - do not submit code or assets that you do not have the right to contribute;
73
+ - preserve required third-party license and attribution notices.
74
+
75
+ Accepted contributions are contributed under Apache-2.0.
76
+
77
+ ## Releasing
78
+
79
+ Releases are made by maintainers. A merge to `main` publishes the container image
80
+ `ghcr.io/kaitencloud/kaiten-mcp:main`. To release a version, set `version` in
81
+ `pyproject.toml`, merge it, and push the tag:
82
+
83
+ ```bash
84
+ git tag v0.3.0
85
+ git push origin v0.3.0
86
+ ```
87
+
88
+ CI then publishes the image as `:0.3.0` and `:0.3`, and the Python package to PyPI as
89
+ [`kaitencloud-mcp`](https://pypi.org/project/kaitencloud-mcp/). The `publish` job refuses a
90
+ tag that does not match `pyproject.toml`'s version.
91
+
92
+ PyPI publishing uses trusted publishing, so no token is stored: the project's publisher on
93
+ PyPI must name this repository, the `CI` workflow (`ci.yml`) and the `pypi` environment.
94
+ The image's package is created by the first publish and takes the repository's visibility,
95
+ so the repository must be public before then.
96
+
97
+ ## Security
98
+
99
+ Do not report unpatched vulnerabilities in public issues.
100
+
101
+ See [SECURITY.md](./SECURITY.md).
@@ -0,0 +1,37 @@
1
+ Developer Certificate of Origin
2
+ Version 1.1
3
+
4
+ Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
5
+ 1 Letterman Drive
6
+ Suite D4700
7
+ San Francisco, CA, 94129
8
+
9
+ Everyone is permitted to copy and distribute verbatim copies of this
10
+ license document, but changing it is not allowed.
11
+
12
+
13
+ Developer's Certificate of Origin 1.1
14
+
15
+ By making a contribution to this project, I certify that:
16
+
17
+ (a) The contribution was created in whole or in part by me and I
18
+ have the right to submit it under the open source license
19
+ indicated in the file; or
20
+
21
+ (b) The contribution is based upon previous work that, to the best
22
+ of my knowledge, is covered under an appropriate open source
23
+ license and I have the right under that license to submit that
24
+ work with modifications, whether created in whole or in part
25
+ by me, under the same open source license (unless I am
26
+ permitted to submit under a different license), as indicated
27
+ in the file; or
28
+
29
+ (c) The contribution was provided directly to me by some other
30
+ person who certified (a), (b) or (c) and I have not modified
31
+ it.
32
+
33
+ (d) I understand and agree that this project and the contribution
34
+ are public and that a record of the contribution (including all
35
+ personal information I submit with it, including my sign-off) is
36
+ maintained indefinitely and may be redistributed consistent with
37
+ this project or the open source license(s) involved.
@@ -0,0 +1,30 @@
1
+ # Kaiten MCP server.
2
+ #
3
+ # docker run -p 8000:8000 -e KAITEN_API_BASE_URL=https://kaiten.example.com/api ghcr.io/kaitencloud/kaiten-mcp
4
+ # Streamable HTTP at /mcp; each request authenticates with its own bearer token.
5
+ # docker run -i --rm -e KAITEN_API_BASE_URL=... -e KAITEN_API_TOKEN=ksh_... ghcr.io/kaitencloud/kaiten-mcp stdio
6
+ # stdio, for a local client that spawns the server.
7
+
8
+ ARG PYTHON_BASE=3.13-slim
9
+
10
+ FROM python:$PYTHON_BASE AS builder
11
+ COPY --from=ghcr.io/astral-sh/uv:0.9.1 /uv /uvx /bin/
12
+ ENV UV_COMPILE_BYTECODE=1 UV_LINK_MODE=copy
13
+ WORKDIR /app
14
+ # Dependencies first, so a source change does not reinstall them.
15
+ COPY pyproject.toml uv.lock ./
16
+ RUN uv sync --locked --no-dev --no-install-project
17
+ COPY README.md LICENSE NOTICE ./
18
+ COPY src ./src
19
+ RUN uv sync --locked --no-dev --no-editable
20
+
21
+ FROM python:$PYTHON_BASE
22
+ WORKDIR /app
23
+ COPY --from=builder --chown=1000:1000 /app/.venv /app/.venv
24
+ USER 1000
25
+ ENV PATH="/app/.venv/bin:$PATH" PYTHONUNBUFFERED=1 MCP_PORT=8000
26
+ EXPOSE 8000
27
+ HEALTHCHECK --interval=30s --timeout=10s --start-period=10s --retries=3 \
28
+ CMD python -c "import os, urllib.request; urllib.request.urlopen(f'http://127.0.0.1:{os.environ[\"MCP_PORT\"]}/health', timeout=5)" || exit 1
29
+ ENTRYPOINT ["kaiten-mcp"]
30
+ CMD ["http"]