@sohampatwardhan/kiro-mcp-server 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +217 -2
  3. package/dist/src/acp/host.js +279 -0
  4. package/dist/src/acp/jsonrpc.js +223 -0
  5. package/dist/src/acp/types.js +5 -0
  6. package/dist/src/argv.js +162 -0
  7. package/dist/src/children.js +135 -0
  8. package/dist/src/cli.js +111 -0
  9. package/dist/src/config.js +113 -0
  10. package/dist/src/core.js +28 -0
  11. package/dist/src/errors.js +56 -0
  12. package/dist/src/events.js +374 -0
  13. package/dist/src/headless.js +107 -0
  14. package/dist/src/index.js +50 -0
  15. package/dist/src/jobs.js +128 -0
  16. package/dist/src/parse.js +100 -0
  17. package/dist/src/paths.js +96 -0
  18. package/dist/src/permissions.js +236 -0
  19. package/dist/src/prereq.js +63 -0
  20. package/dist/src/prompt.js +131 -0
  21. package/dist/src/server.js +118 -0
  22. package/dist/src/sessions.js +429 -0
  23. package/dist/src/specs.js +104 -0
  24. package/dist/src/tangent.js +76 -0
  25. package/dist/src/tools/branching.js +89 -0
  26. package/dist/src/tools/config.js +253 -0
  27. package/dist/src/tools/configure.js +258 -0
  28. package/dist/src/tools/define.js +27 -0
  29. package/dist/src/tools/extensions.js +209 -0
  30. package/dist/src/tools/headless.js +156 -0
  31. package/dist/src/tools/history.js +120 -0
  32. package/dist/src/tools/permissions.js +42 -0
  33. package/dist/src/tools/schemas.js +40 -0
  34. package/dist/src/tools/sessions.js +333 -0
  35. package/dist/src/tools/specs.js +31 -0
  36. package/dist/src/tools/status.js +210 -0
  37. package/dist/src/tools/stream.js +159 -0
  38. package/dist/src/tools/turns.js +254 -0
  39. package/dist/src/tools/views.js +42 -0
  40. package/dist/src/transcript.js +107 -0
  41. package/dist/src/trust.js +40 -0
  42. package/dist/src/turns.js +286 -0
  43. package/package.json +52 -4
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Soham Patwardhan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,218 @@
1
- # Temporary Holding Version
1
+ # Kiro MCP Server
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ [![npm version](https://img.shields.io/npm/v/@sohampatwardhan/kiro-mcp-server.svg)](https://www.npmjs.com/package/@sohampatwardhan/kiro-mcp-server)
4
+ [![CI](https://github.com/sohampatwardhan/kiro-mcp-server/actions/workflows/ci.yml/badge.svg)](https://github.com/sohampatwardhan/kiro-mcp-server/actions/workflows/ci.yml)
5
+
6
+ MCP server that lets AI agents with an always-on computer (Grok Bot, Claude, Cursor, Codex, and other MCP clients) orchestrate [Kiro CLI V3](https://kiro.dev/docs/cli/v3.md) coding sessions through typed tools instead of shell commands. It is the Kiro sibling of [`claude-code-mcp-server`](https://github.com/sohampatwardhan/claude-code-mcp-server).
7
+
8
+ > **Requires `kiro-cli` with the V3 engine, installed and signed in, on the machine that runs this server.**
9
+ >
10
+ > 1. Install it: `curl -fsSL https://cli.kiro.dev/install | bash` (or `brew install kiro-cli` on macOS).
11
+ > 2. Sign in: run `kiro-cli login` in a terminal (`kiro-cli login --use-device-flow` on a machine without a browser).
12
+ > 3. Trust each workspace once: run `kiro-cli --v3` in the folder and choose **Trust, continue** (see [Workspace trust](#workspace-trust)).
13
+ >
14
+ > Without the CLI, tools return `CLI_NOT_FOUND` with the install command, and the server prints the same warning on stderr at startup. A signed-out CLI gives `CLI_NOT_SIGNED_IN`. This server never logs in for you.
15
+
16
+ The server drives Kiro three ways: one long-lived ACP host (`kiro-cli acp --agent-engine=v3 --auth-method=cli`, newline-delimited JSON-RPC) for sessions, prompts, streaming, permissions, and Kiro's extension methods; headless runs (`kiro-cli chat --v3 --no-interactive --output-format stream-json`) for fire-and-forget work; and a few fixed-argv subcommands (`whoami`, `settings`, `agent`, `mcp`, `powers`). It exposes 32 tools and one subscribable transcript resource. The full feature-parity matrix, with what is exposed, composed, gated, withheld, or impossible and why, is in [`.kiro/specs/kiro-v3-orchestration/requirements.md`](.kiro/specs/kiro-v3-orchestration/requirements.md#feature-parity-matrix).
17
+
18
+ ## Design
19
+
20
+ - **Strict schemas, typed results.** Every tool has a strict input schema (`additionalProperties: false`) and an `outputSchema`, and returns `structuredContent`. Errors are `{ "error": { "code", "message", "details" } }` with a code from the [error table](#errors).
21
+ - **No escape hatch.** No shell tool, no raw argv, no field that forwards free-form CLI flags, and no "send any ACP method" tool. Kiro commands that only exist in the terminal UI are mapped to typed tools, or listed as not possible.
22
+ - **Fixed argv.** The only place `kiro-cli` argv is built is `src/argv.ts`. Values are attached as `--flag=value` or follow `--`; prompts go on stdin. The v2-only `--agent`, `--model`, `--effort`, and `--trust-*` flags are never passed to `acp`: agent mode, model, and effort are set through session options.
23
+ - **Operator policy.** Kiro runs only inside `KIRO_MCP_ROOTS` (checked with `realpath`). Shell approvals, persistent consent, headless trust flags, and config writes each need an explicit opt-in.
24
+ - **Bounded processes.** Every child is spawned detached in its own process group and tracked. Closing stdin, SIGINT, SIGTERM, or SIGHUP cancels running turns, closes the host's stdin, and stops every process group.
25
+ - **In memory.** Event feeds, jobs, tangent trees, pending permissions, and queued prompts live in memory. The server never writes Kiro's session files. No telemetry; no tokens, `KIRO_API_KEY`, or authorization URLs in output or logs.
26
+
27
+ ## Prerequisites
28
+
29
+ - Node.js 20 or later.
30
+ - `kiro-cli` 2.37 or later with the V3 engine (`kiro-cli acp --agent-engine=v3` must start), signed in. Set `KIRO_BIN` when `kiro-cli` is not on the server's `PATH`.
31
+
32
+ ## Install
33
+
34
+ ```bash
35
+ npx -y @sohampatwardhan/kiro-mcp-server@latest
36
+ ```
37
+
38
+ ### Claude Code
39
+
40
+ ```bash
41
+ claude mcp add kiro -e KIRO_MCP_ROOTS=$HOME/code -- npx -y @sohampatwardhan/kiro-mcp-server@latest
42
+ ```
43
+
44
+ ### Cursor, Claude Desktop, and other JSON configs
45
+
46
+ ```json
47
+ {
48
+ "mcpServers": {
49
+ "kiro": {
50
+ "command": "npx",
51
+ "args": ["-y", "@sohampatwardhan/kiro-mcp-server@latest"],
52
+ "env": { "KIRO_MCP_ROOTS": "/Users/me/code" }
53
+ }
54
+ }
55
+ }
56
+ ```
57
+
58
+ ### Codex
59
+
60
+ ```toml
61
+ [mcp_servers.kiro]
62
+ command = "npx"
63
+ args = ["-y", "@sohampatwardhan/kiro-mcp-server@latest"]
64
+ env = { KIRO_MCP_ROOTS = "/home/me/code" }
65
+ ```
66
+
67
+ ## Install the skills
68
+
69
+ The repository ships agent skills that teach agents when and how to use these tools: delegating a task to Kiro, choosing mode, model, effort, and permission handling, following a run live, collecting results and patches, and recovering from failures. Install them with the [skills CLI](https://github.com/vercel-labs/skills):
70
+
71
+ ```bash
72
+ npx skills add sohampatwardhan/kiro-mcp-server -g
73
+ ```
74
+
75
+ Install one skill, or only for some agents:
76
+
77
+ ```bash
78
+ npx skills add sohampatwardhan/kiro-mcp-server -g --skill kiro-orchestration
79
+ npx skills add sohampatwardhan/kiro-mcp-server -g -a claude-code -a codex
80
+ ```
81
+
82
+ Each skill's `SKILL.md` carries a SemVer `metadata.version`, checked in CI by `scripts/check-skill-versions.mjs`, and CI installs the skills from the checkout into a throwaway `HOME` to test this exact path.
83
+
84
+ ## Environment variables
85
+
86
+ All are optional. An invalid value makes the server print the variable name and reason on stderr and exit with code 2. Booleans accept `1`, `true`, `yes`, `0`, `false`, `no`.
87
+
88
+ | Variable | Default | Effect |
89
+ | --- | --- | --- |
90
+ | `KIRO_BIN` | `kiro-cli` | CLI path or name |
91
+ | `KIRO_MCP_CWD` | process cwd | Default root when `KIRO_MCP_ROOTS` is unset |
92
+ | `KIRO_MCP_ROOTS` | `KIRO_MCP_CWD` | Comma-separated absolute directories Kiro may run in. `/` is refused. The first is the ACP host's cwd |
93
+ | `KIRO_MCP_PERMISSIONS` | `ask` | `ask` (the calling agent answers), `deny` (reject all), or `allow` (approve once; shell still follows `KIRO_MCP_ALLOW_SHELL`) |
94
+ | `KIRO_MCP_PERMISSION_TIMEOUT_MS` | `600000` | Unanswered permissions are rejected after this long |
95
+ | `KIRO_MCP_ALLOW_SHELL` | off | Lets shell-command requests be approved. Off: they are rejected at once |
96
+ | `KIRO_MCP_ALLOW_PERSISTENT_CONSENT` | off | Allows `allow_always` answers |
97
+ | `KIRO_MCP_HEADLESS_TRUST` | `none` | `tools` allows headless `trust_tools`; `all` also allows `trust_all_tools` |
98
+ | `KIRO_MCP_ALLOW_TRUST_WORKSPACE` | off | Allows headless `trust_workspace` |
99
+ | `KIRO_MCP_ALLOW_CONFIG_WRITES` | off | Allows agent, MCP config, settings, and powers writes |
100
+ | `KIRO_MCP_CLI_TIMEOUT_MS` | `120000` | Subcommand timeout |
101
+ | `KIRO_MCP_REQUEST_TIMEOUT_MS` | `120000` | ACP request timeout (prompts have none) |
102
+ | `KIRO_MCP_HEADLESS_TIMEOUT_MS` | `3600000` | Headless job timeout; `timeout_s` may lower it |
103
+
104
+ `KIRO_API_KEY`, when set in the server's environment, is inherited by headless runs only. No tool can read or set it.
105
+
106
+ ### Workspace trust
107
+
108
+ ACP has no method for trusting a workspace. Until a folder is trusted, Kiro does not load its custom agents, steering, MCP servers, skills, or workflows, and asks before every shell and MCP call. Trust each root once: run `kiro-cli --v3` in the folder and choose **Trust, continue**. `kiro_status` reports the saved trust for every root (read-only, from Kiro's per-workspace permissions file) and warns for each untrusted one. Kiro never saves trust for your home directory.
109
+
110
+ ### Shell approvals
111
+
112
+ Shell approvals are off by default, as in `claude-code-mcp-server`: Kiro's requests to run commands (including tests and builds) are rejected at once and recorded as `policy: "shell_disabled"`. To let the calling agent approve them one at a time, set `KIRO_MCP_ALLOW_SHELL=1`. To approve every non-shell request automatically as well, add `KIRO_MCP_PERMISSIONS=allow`.
113
+
114
+ ## Typical flow
115
+
116
+ 1. `kiro_status` checks the CLI, sign-in, V3 engine, trust, and policy.
117
+ 2. `kiro_session_new` (or `kiro_session_load`) attaches a session; `kiro_session_configure` picks mode (`vibe`, `plan`, `spec`, `quick-spec`, `bug-fix`, `autonomous`, or a custom agent), model, and effort.
118
+ 3. `kiro_prompt` sends a prompt and waits up to `wait_s`. If the turn is still running, poll `kiro_events` (or `kiro_stream_session`) with the returned cursor until the status is `completed`, `cancelled`, or `failed`.
119
+ 4. Answer `pending_permissions` with `kiro_permission_respond`. When the client supports MCP elicitation, the request is also shown to its user.
120
+ 5. Steer with `kiro_steer`, stop with `kiro_cancel`, branch with `kiro_session_fork` or `kiro_tangent`, and close with `kiro_session_close`.
121
+
122
+ ## Tool reference
123
+
124
+ | Tool | What it does |
125
+ | --- | --- |
126
+ | `kiro_status` | CLI path and version, V3 engine, capabilities, sign-in (account type, email, profile), roots and trust, policy, log paths, optional diagnostics |
127
+ | `kiro_usage` | Plan usage and credits (`_kiro/account/getUsage`), plus credits observed for a session |
128
+ | `kiro_session_new` | New local or cloud session, with mode, model, effort, title |
129
+ | `kiro_session_list` | Saved sessions by workspace or user scope, with source, title, and limit filters |
130
+ | `kiro_session_load` | Resume a session by id, or the latest for a workspace, with replay |
131
+ | `kiro_session_configure` | Mode, model, effort, other advertised options, title |
132
+ | `kiro_session_state` | MCP servers, tools, steering, skills, powers, governance, agent errors, commands, modes, config, context, `/config` panel, models |
133
+ | `kiro_session_history` | Paged transcript and rewind points |
134
+ | `kiro_session_fork` | Fork, optionally at a user message (`/rewind`) |
135
+ | `kiro_session_export` | Export to a file inside the roots |
136
+ | `kiro_session_compact` | Compact an idle session's context |
137
+ | `kiro_session_close` | Close a session (cancels its turn, rejects its permissions) |
138
+ | `kiro_session_delete` | Delete a session from Kiro's store (`confirm: true`) |
139
+ | `kiro_prompt` | Prompt with text, files, images, or an advertised command; wait or queue |
140
+ | `kiro_events` | Cursor-based event feed for a session or headless job, with long-poll |
141
+ | `kiro_steer` | Steer a running turn |
142
+ | `kiro_cancel` | Cancel a running turn, or stop a headless job |
143
+ | `kiro_permissions` | Pending permission requests |
144
+ | `kiro_permission_respond` | Answer one with an offered option |
145
+ | `kiro_tangent` | Branch, merge, and list tangents |
146
+ | `kiro_specs` | List and read `.kiro/specs` documents with parsed tasks |
147
+ | `kiro_memory` | List, read, create, update, delete memories; sync status |
148
+ | `kiro_knowledge` | Knowledge base subcommands |
149
+ | `kiro_code_intelligence` | Code intelligence subcommands |
150
+ | `kiro_workflow` | Workflow recipes and runs |
151
+ | `kiro_cloud_sources` | Connected source providers and repositories |
152
+ | `kiro_run_headless` | Headless run as a job |
153
+ | `kiro_agents` | List and validate custom agents; create, set default, upgrade (gated) |
154
+ | `kiro_mcp_config` | List and status of MCP config; add HTTP servers and remove (gated) |
155
+ | `kiro_settings` | List and get settings; set and delete (gated, sensitive keys withheld) |
156
+ | `kiro_powers` | List powers; install and uninstall (gated) |
157
+ | `kiro_stream_session` | Follow any session's transcript live with a cursor |
158
+
159
+ Resource: `kiro-session://<session_id>/transcript` returns a session's newest events as JSON. Subscribe with `resources/subscribe` to get `notifications/resources/updated` within a second of new events.
160
+
161
+ ## Errors
162
+
163
+ | Code | Meaning |
164
+ | --- | --- |
165
+ | `CLI_NOT_FOUND` | `kiro-cli` (or `KIRO_BIN`) is missing; the message has the install command |
166
+ | `CLI_NOT_SIGNED_IN` | Kiro reports a missing login; run `kiro-cli login` |
167
+ | `ENGINE_V3_UNAVAILABLE` | `kiro-cli acp --agent-engine=v3` did not initialize; names the CLI version and reason |
168
+ | `HOST_EXITED` | The ACP host exited during the call; the session re-attaches on next use and the prompt is not resent |
169
+ | `UNSUPPORTED_BY_KIRO` | Kiro does not advertise or implement the method or target |
170
+ | `KIRO_ERROR` | Other Kiro JSON-RPC error; `details.kiro` has Kiro's code, message, and error type |
171
+ | `KIRO_REFUSED` | Kiro answered `success: false` or refused for trust or access level |
172
+ | `OUTPUT_PARSE_FAILED` | CLI or Kiro output could not be parsed |
173
+ | `TIMEOUT` | A subcommand, request, or job passed its timeout; its process group was stopped |
174
+ | `CLI_EXIT` | A subcommand exited nonzero; `details.cli` has cleaned output tails |
175
+ | `PATH_OUTSIDE_ROOTS` | A path resolves outside `KIRO_MCP_ROOTS` |
176
+ | `PATH_EXISTS` | Export destination exists and `overwrite` is not true |
177
+ | `CONTENT_TOO_LARGE` | A file or image is too large, not UTF-8, or not an allowed image type |
178
+ | `SESSION_NOT_FOUND` | The session is unknown to Kiro or not attached here |
179
+ | `TURN_ACTIVE` | The session already has a running turn |
180
+ | `NO_ACTIVE_TURN` | Steering needs a running turn |
181
+ | `QUEUE_FULL` | Five prompts already wait for this session |
182
+ | `VALUE_NOT_ADVERTISED` | Mode, option, value, command, or recipe input is not advertised; lists the allowed values |
183
+ | `PERMISSION_NOT_FOUND` | The permission is not pending |
184
+ | `OPTION_NOT_OFFERED` | The option id was not offered |
185
+ | `PERSISTENT_CONSENT_DISABLED` | `allow_always` needs `KIRO_MCP_ALLOW_PERSISTENT_CONSENT` |
186
+ | `SHELL_APPROVAL_DISABLED` | Shell approvals need `KIRO_MCP_ALLOW_SHELL` |
187
+ | `HEADLESS_TRUST_DISABLED` | A headless trust flag is not allowed by the operator |
188
+ | `CONFIG_WRITES_DISABLED` | Config writes need `KIRO_MCP_ALLOW_CONFIG_WRITES` |
189
+ | `STDIO_SERVER_WITHHELD` | Stdio MCP servers run arbitrary commands and cannot be added |
190
+ | `SETTING_WITHHELD` | The setting key is withheld |
191
+ | `CONFIRMATION_REQUIRED` | The call needs `confirm: true` |
192
+ | `JOB_NOT_FOUND` | Unknown or expired headless job |
193
+ | `SPEC_NOT_FOUND` | The spec or document does not exist |
194
+ | `TANGENT_NOT_FOUND` | Not part of a tangent tree, or no tangent with that name |
195
+ | `INVALID_MERGE_TARGET` | The merge destination resolves to the tangent itself or nothing |
196
+ | `NOTHING_TO_BRANCH` | The session has no messages yet |
197
+
198
+ ## Development
199
+
200
+ ```bash
201
+ npm ci
202
+ npm run build # strict tsc is the static check
203
+ npm test # builds, then node --test against the fake kiro-cli
204
+ ```
205
+
206
+ Tests never run a real `kiro-cli`: `test/fixtures/fake-kiro.mjs` stands in for it through `KIRO_BIN`. See [AGENTS.md](AGENTS.md) for the architecture and rules.
207
+
208
+ ## Releasing
209
+
210
+ Bump `version` in `package.json`, merge, then push a matching tag (`git tag v0.1.0 && git push origin v0.1.0`). The release workflow checks the tag against `package.json`, builds, tests, publishes to npm with provenance through trusted publishing (GitHub OIDC, no `NPM_TOKEN`), creates a GitHub release, and mirrors the package to GitHub Packages.
211
+
212
+ ## Credits
213
+
214
+ Inspired by [vanphappi/kiro-cli-mcp](https://github.com/vanphappi/kiro-cli-mcp). No code was copied.
215
+
216
+ ## License
217
+
218
+ MIT
@@ -0,0 +1,279 @@
1
+ import { spawn } from "node:child_process";
2
+ import { EventEmitter } from "node:events";
3
+ import { acpArgv } from "../argv.js";
4
+ import { KILL_GRACE_MS, killGroup, track } from "../children.js";
5
+ import { runCli } from "../cli.js";
6
+ import { LIMITS } from "../config.js";
7
+ import { ToolError } from "../errors.js";
8
+ import { cleanTerminalText, tail } from "../parse.js";
9
+ import { cliNotFoundMessage, isSignInError, parseVersion, signInMessage } from "../prereq.js";
10
+ import { JsonRpcPeer, METHOD_NOT_FOUND, RpcClosedError, RpcError, RpcTimeoutError } from "./jsonrpc.js";
11
+ /**
12
+ * Optional methods that work on Kiro CLI 2.37.1 without being advertised in
13
+ * `extensionMethods` (design Decision 6).
14
+ */
15
+ export const KNOWN_UNADVERTISED = ["_kiro/account/getUsage", "_kiro/session/rename", "_session/steer"];
16
+ /**
17
+ * The long-lived `kiro-cli acp --agent-engine=v3` child. Only this module
18
+ * writes to the host process. Started lazily; restarted on the next call
19
+ * after it exits. Each start bumps `generation`, so sessions can tell that
20
+ * they must be re-attached.
21
+ */
22
+ export class AcpHost extends EventEmitter {
23
+ opts;
24
+ state = "stopped";
25
+ generation = 0;
26
+ capabilities = null;
27
+ initResult = null;
28
+ lastError = null;
29
+ child = null;
30
+ peer = null;
31
+ starting = null;
32
+ lastDownAt = 0;
33
+ stderrText = "";
34
+ droppedBefore = 0;
35
+ notificationHandlers = [];
36
+ anyHandlers = [];
37
+ requestHandlers = [];
38
+ constructor(opts) {
39
+ super();
40
+ this.opts = opts;
41
+ }
42
+ /** `agentCapabilities._meta.kiro` from the last initialize. */
43
+ get kiro() {
44
+ return this.capabilities?._meta?.kiro ?? {};
45
+ }
46
+ /** Lines dropped across every host generation (Requirement 5.7). */
47
+ get droppedLines() {
48
+ return this.droppedBefore + (this.peer?.droppedLines ?? 0);
49
+ }
50
+ onNotification(method, handler) {
51
+ this.notificationHandlers.push([method, handler]);
52
+ this.peer?.onNotification(method, handler);
53
+ }
54
+ onAnyNotification(handler) {
55
+ this.anyHandlers.push(handler);
56
+ this.peer?.onAnyNotification(handler);
57
+ }
58
+ onRequest(method, handler) {
59
+ this.requestHandlers.push([method, handler]);
60
+ this.peer?.onRequest(method, handler);
61
+ }
62
+ /** True for core ACP methods, advertised extension methods, and KNOWN_UNADVERTISED. */
63
+ supports(method) {
64
+ if (!method.startsWith("_"))
65
+ return true;
66
+ if (KNOWN_UNADVERTISED.includes(method))
67
+ return true;
68
+ return (this.kiro.extensionMethods ?? []).includes(method);
69
+ }
70
+ /** Start the host if it is not running. Concurrent callers share one start. */
71
+ ensureStarted() {
72
+ if (this.state === "ready" && this.peer !== null && !this.peer.closed)
73
+ return Promise.resolve();
74
+ if (this.starting !== null)
75
+ return this.starting;
76
+ this.starting = this.start().finally(() => {
77
+ this.starting = null;
78
+ });
79
+ return this.starting;
80
+ }
81
+ async start() {
82
+ const backoff = this.opts.restartBackoffMs ?? 250;
83
+ const wait = this.lastDownAt + backoff - Date.now();
84
+ if (wait > 0)
85
+ await new Promise((r) => setTimeout(r, wait));
86
+ this.state = "starting";
87
+ this.stderrText = "";
88
+ this.generation++;
89
+ const generation = this.generation;
90
+ let child;
91
+ try {
92
+ child = spawn(this.opts.bin, acpArgv(), {
93
+ cwd: this.opts.cwd,
94
+ stdio: ["pipe", "pipe", "pipe"],
95
+ shell: false,
96
+ detached: true,
97
+ windowsHide: true,
98
+ env: { ...(this.opts.env ?? process.env), NO_COLOR: "1" },
99
+ });
100
+ }
101
+ catch (err) {
102
+ throw this.fail(new ToolError("CLI_NOT_FOUND", cliNotFoundMessage(this.opts.bin), { reason: String(err) }));
103
+ }
104
+ track(child);
105
+ this.child = child;
106
+ child.stderr.on("data", (chunk) => {
107
+ this.stderrText = tail(this.stderrText + chunk.toString("utf8"), LIMITS.outputTailChars);
108
+ });
109
+ child.stdin.on("error", () => undefined);
110
+ const peer = new JsonRpcPeer(child.stdout, child.stdin, { maxLineBytes: LIMITS.maxLineBytes });
111
+ this.peer = peer;
112
+ for (const [method, handler] of this.requestHandlers)
113
+ peer.onRequest(method, handler);
114
+ for (const [method, handler] of this.notificationHandlers)
115
+ peer.onNotification(method, handler);
116
+ for (const handler of this.anyHandlers)
117
+ peer.onAnyNotification(handler);
118
+ let spawnFailure = null;
119
+ const spawnError = new Promise((_, reject) => {
120
+ child.once("error", (err) => {
121
+ spawnFailure =
122
+ err.code === "ENOENT" || err.code === "EACCES"
123
+ ? new ToolError("CLI_NOT_FOUND", cliNotFoundMessage(this.opts.bin))
124
+ : new ToolError("ENGINE_V3_UNAVAILABLE", `failed to start kiro-cli acp: ${err.message}`);
125
+ reject(spawnFailure);
126
+ peer.close("spawn error");
127
+ });
128
+ });
129
+ spawnError.catch(() => undefined);
130
+ let exitedEarly = false;
131
+ const onDown = (reason) => {
132
+ if (this.generation !== generation)
133
+ return;
134
+ exitedEarly = this.state === "starting";
135
+ if (this.state === "ready") {
136
+ this.state = "exited";
137
+ this.lastDownAt = Date.now();
138
+ this.droppedBefore += peer.droppedLines;
139
+ this.emit("exited", generation, reason);
140
+ }
141
+ };
142
+ child.once("exit", (code, signal) => {
143
+ peer.close(`host exited (${code ?? signal})`);
144
+ onDown(`host exited with ${code ?? signal}`);
145
+ });
146
+ peer.onClose((reason) => {
147
+ onDown(reason);
148
+ // stdout closed while the process lives on: stop it so it does not linger. During
149
+ // stop() the host gets to drain its stdin (a final session/cancel) and exit on its own.
150
+ if (this.state !== "stopping" && child.exitCode === null && child.signalCode === null)
151
+ killGroup(child, "SIGTERM");
152
+ });
153
+ try {
154
+ const init = (await Promise.race([
155
+ peer.request("initialize", {
156
+ protocolVersion: 1,
157
+ clientCapabilities: { fs: { readTextFile: false, writeTextFile: false }, terminal: false },
158
+ clientInfo: { name: "kiro-mcp-server", version: this.opts.clientVersion ?? "0.0.0" },
159
+ }, this.opts.requestTimeoutMs),
160
+ spawnError,
161
+ ]));
162
+ if (init === null || typeof init !== "object")
163
+ throw new RpcError(-32603, "initialize returned no result");
164
+ this.initResult = init;
165
+ this.capabilities = init.agentCapabilities ?? {};
166
+ this.state = "ready";
167
+ this.lastError = null;
168
+ this.emit("started", generation);
169
+ }
170
+ catch (err) {
171
+ if (spawnFailure !== null)
172
+ throw this.fail(spawnFailure);
173
+ if (err instanceof ToolError)
174
+ throw this.fail(err);
175
+ // Let the exit (or stderr) land so the message can name the reason.
176
+ await new Promise((r) => setTimeout(r, exitedEarly ? 0 : 50));
177
+ killGroup(child, "SIGKILL");
178
+ const version = await this.cliVersion();
179
+ const reason = cleanTerminalText(this.stderrText).trim().split("\n").slice(-3).join(" ") || (err instanceof Error ? err.message : String(err));
180
+ throw this.fail(new ToolError("ENGINE_V3_UNAVAILABLE", `kiro-cli ${version ?? "(unknown version)"} could not start the V3 ACP engine: ${reason}`, {
181
+ cliVersion: version,
182
+ stderrTail: tail(cleanTerminalText(this.stderrText), 2_000),
183
+ }));
184
+ }
185
+ }
186
+ fail(err) {
187
+ this.state = "failed";
188
+ this.lastDownAt = Date.now();
189
+ this.lastError = err;
190
+ return err;
191
+ }
192
+ async cliVersion() {
193
+ try {
194
+ const result = await runCli(this.opts.bin, ["--version"], { timeoutMs: 10_000, cwd: this.opts.cwd, env: this.opts.env });
195
+ return parseVersion(result.stdout);
196
+ }
197
+ catch {
198
+ return null;
199
+ }
200
+ }
201
+ /**
202
+ * Send a request and map failures to tool errors: UNSUPPORTED_BY_KIRO for
203
+ * an unsupported optional method or method-not-found, CLI_NOT_SIGNED_IN for
204
+ * authentication errors, HOST_EXITED when the host goes away, TIMEOUT, and
205
+ * KIRO_ERROR with `details.kiro` otherwise. Params never appear in errors.
206
+ */
207
+ async call(method, params, opts = {}) {
208
+ if (!this.supports(method)) {
209
+ throw new ToolError("UNSUPPORTED_BY_KIRO", `Kiro does not advertise ${method}`, { method });
210
+ }
211
+ await this.ensureStarted();
212
+ const peer = this.peer;
213
+ if (peer === null)
214
+ throw new ToolError("HOST_EXITED", "the Kiro ACP host is not running");
215
+ const timeoutMs = opts.timeoutMs === undefined ? this.opts.requestTimeoutMs : opts.timeoutMs ?? undefined;
216
+ try {
217
+ return await peer.request(method, params, timeoutMs);
218
+ }
219
+ catch (err) {
220
+ throw mapRpcError(method, err);
221
+ }
222
+ }
223
+ notify(method, params) {
224
+ this.peer?.notify(method, params);
225
+ }
226
+ /** Close the host's stdin, then stop its process group if it does not exit. */
227
+ async stop() {
228
+ const child = this.child;
229
+ if (child === null) {
230
+ this.state = "stopped";
231
+ return;
232
+ }
233
+ this.state = "stopping";
234
+ const exited = new Promise((resolve) => {
235
+ if (child.exitCode !== null || child.signalCode !== null)
236
+ resolve();
237
+ else
238
+ child.once("exit", () => resolve());
239
+ });
240
+ try {
241
+ child.stdin.end();
242
+ }
243
+ catch {
244
+ // already closed
245
+ }
246
+ const timer = setTimeout(() => killGroup(child, "SIGTERM"), 1_000);
247
+ const killTimer = setTimeout(() => killGroup(child, "SIGKILL"), 1_000 + KILL_GRACE_MS);
248
+ await exited;
249
+ clearTimeout(timer);
250
+ clearTimeout(killTimer);
251
+ this.generation++;
252
+ this.peer?.close("server stopping");
253
+ this.child = null;
254
+ this.peer = null;
255
+ this.state = "stopped";
256
+ }
257
+ }
258
+ export function mapRpcError(method, err) {
259
+ if (err instanceof ToolError)
260
+ return err;
261
+ if (err instanceof RpcClosedError)
262
+ return new ToolError("HOST_EXITED", `the Kiro ACP host exited before ${method} answered`);
263
+ if (err instanceof RpcTimeoutError)
264
+ return new ToolError("TIMEOUT", err.message, { method });
265
+ if (err instanceof RpcError) {
266
+ const data = (typeof err.data === "object" && err.data !== null ? err.data : {});
267
+ const errorType = typeof data.errorType === "string" ? data.errorType : undefined;
268
+ const detail = typeof data.details === "string" && data.details.length <= 500 ? data.details : undefined;
269
+ const message = detail !== undefined && /^internal error$/i.test(err.message) ? detail : err.message;
270
+ const kiro = { code: err.code, message, ...(errorType !== undefined ? { errorType } : {}) };
271
+ if (err.code === METHOD_NOT_FOUND)
272
+ return new ToolError("UNSUPPORTED_BY_KIRO", `Kiro does not implement ${method}`, { method, kiro });
273
+ if ((errorType !== undefined && /auth/i.test(errorType)) || isSignInError(message)) {
274
+ return new ToolError("CLI_NOT_SIGNED_IN", signInMessage(), { kiro });
275
+ }
276
+ return new ToolError("KIRO_ERROR", `${method} failed: ${message}`, { method, kiro });
277
+ }
278
+ return new ToolError("KIRO_ERROR", `${method} failed: ${err instanceof Error ? err.message : String(err)}`, { method });
279
+ }