@kata-sh/cli 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Kata CLI
2
2
 
3
- A terminal coding agent built on [pi](https://github.com/badlogic/pi-mono) (`@mariozechner/pi-coding-agent`). Kata CLI bundles a curated set of extensions for structured planning, browser automation, web search, subagent orchestration, and more.
3
+ A terminal coding agent built on [pi](https://github.com/badlogic/pi-mono) (`@mariozechner/pi-coding-agent`). Kata CLI bundles a curated set of extensions for structured planning, browser automation, web search, subagent orchestration, MCP server integration, and more.
4
4
 
5
5
  ## Quick Start
6
6
 
@@ -28,7 +28,7 @@ Kata CLI is a thin wrapper around pi-coding-agent. It does not fork pi — it co
28
28
  apps/cli/
29
29
  src/
30
30
  loader.ts — Entry point: sets KATA_* env vars, imports cli.ts
31
- cli.ts — Calls pi-coding-agent's main()
31
+ cli.ts — Calls createAgentSession() + InteractiveMode
32
32
  app-paths.ts — ~/.kata-cli/ path constants
33
33
  resource-loader.ts — Syncs bundled resources to ~/.kata-cli/agent/
34
34
  wizard.ts — First-run setup, env key hydration
@@ -47,8 +47,12 @@ apps/cli/
47
47
 
48
48
  1. `loader.ts` sets `PI_PACKAGE_DIR` to `pkg/` so pi reads Kata's branding config
49
49
  2. `loader.ts` sets `KATA_CODING_AGENT_DIR` so pi uses `~/.kata-cli/agent/` instead of `~/.pi/agent/`
50
- 3. `resource-loader.ts` syncs bundled extensions, agents, skills, and `AGENTS.md` to `~/.kata-cli/agent/` on every launch
51
- 4. `cli.ts` calls pi-coding-agent's `main()` pi handles everything from there
50
+ 3. `loader.ts` injects `--mcp-config ~/.kata-cli/agent/mcp.json` into `process.argv` for the MCP adapter
51
+ 4. `resource-loader.ts` syncs bundled extensions, agents, skills, and `AGENTS.md` to `~/.kata-cli/agent/` on every launch
52
+ 5. `resource-loader.ts` scaffolds a starter `mcp.json` on first launch (never overwrites existing config)
53
+ 6. `cli.ts` seeds `npm:pi-mcp-adapter` into settings so pi auto-installs it
54
+ 7. `cli.ts` injects the `mcp-config` flag into the extension runtime (required because Kata bypasses pi's `main()` and its two-pass argv parsing)
55
+ 8. `cli.ts` calls `createAgentSession()` + `InteractiveMode` — pi handles everything from there
52
56
 
53
57
  ## Bundled Extensions
54
58
 
@@ -64,6 +68,136 @@ apps/cli/
64
68
  | `mac-tools/` | macOS-specific utilities |
65
69
  | `shared/` | Shared UI components (library, not an entry point) |
66
70
 
71
+ ## MCP Support
72
+
73
+ Kata ships with [MCP](https://modelcontextprotocol.io/) (Model Context Protocol) support via [`pi-mcp-adapter`](https://github.com/nicobailon/pi-mcp-adapter), auto-installed on first launch. One proxy `mcp` tool (~200 tokens in context) gives the agent on-demand access to any MCP server's tools without burning context on individual tool definitions.
74
+
75
+ ### How It Works
76
+
77
+ The MCP integration has three parts:
78
+
79
+ 1. **Package seeding**: `cli.ts` ensures `npm:pi-mcp-adapter` is in the settings packages list on every startup. Pi's package manager auto-installs it globally if missing.
80
+ 2. **Config path injection**: `loader.ts` pushes `--mcp-config` into `process.argv` and `cli.ts` sets the flag on `runtime.flagValues` — both are needed because the adapter reads the config path at two different points in its lifecycle.
81
+ 3. **Config scaffolding**: `resource-loader.ts` creates a starter `~/.kata-cli/agent/mcp.json` on first launch. Never overwrites existing config.
82
+
83
+ ### Adding MCP Servers
84
+
85
+ Edit `~/.kata-cli/agent/mcp.json`:
86
+
87
+ ```json
88
+ {
89
+ "settings": {
90
+ "toolPrefix": "server",
91
+ "idleTimeout": 10
92
+ },
93
+ "mcpServers": {}
94
+ }
95
+ ```
96
+
97
+ #### Example: Linear (OAuth via mcp-remote)
98
+
99
+ Many hosted MCP servers (Linear, etc.) use OAuth 2.1 authentication. These require [`mcp-remote`](https://github.com/geelen/mcp-remote) as a stdio proxy that handles the browser-based OAuth flow:
100
+
101
+ ```json
102
+ {
103
+ "settings": { "toolPrefix": "server", "idleTimeout": 10 },
104
+ "mcpServers": {
105
+ "linear": {
106
+ "command": "npx",
107
+ "args": ["-y", "mcp-remote", "https://mcp.linear.app/mcp"]
108
+ }
109
+ }
110
+ }
111
+ ```
112
+
113
+ After adding the config and restarting Kata:
114
+
115
+ 1. Connect the server (opens browser for OAuth):
116
+ ```
117
+ mcp({ connect: "linear" })
118
+ ```
119
+ 2. Authorize in the browser when prompted by Linear.
120
+ 3. Use tools:
121
+ ```
122
+ mcp({ server: "linear" }) — list all Linear tools
123
+ mcp({ search: "issues" }) — search for issue-related tools
124
+ mcp({ tool: "linear_list_teams" }) — call a tool
125
+ ```
126
+
127
+ Tokens are cached in `~/.mcp-auth/` for subsequent sessions. If you hit errors, clear cached auth with `rm -rf ~/.mcp-auth` and reconnect.
128
+
129
+ #### Example: Stdio server with env vars
130
+
131
+ ```json
132
+ {
133
+ "mcpServers": {
134
+ "my-server": {
135
+ "command": "npx",
136
+ "args": ["-y", "some-mcp-server"],
137
+ "env": {
138
+ "API_KEY": "${MY_API_KEY}"
139
+ }
140
+ }
141
+ }
142
+ }
143
+ ```
144
+
145
+ Environment variables support `${VAR}` interpolation from `process.env`.
146
+
147
+ #### Example: HTTP server with bearer token
148
+
149
+ ```json
150
+ {
151
+ "mcpServers": {
152
+ "my-api": {
153
+ "url": "https://api.example.com/mcp",
154
+ "auth": "bearer",
155
+ "bearerTokenEnv": "MY_API_KEY"
156
+ }
157
+ }
158
+ }
159
+ ```
160
+
161
+ #### Importing existing configs
162
+
163
+ Pull in your existing Claude Code, Cursor, or VS Code MCP configuration:
164
+
165
+ ```json
166
+ {
167
+ "imports": ["claude-code", "cursor"],
168
+ "mcpServers": {}
169
+ }
170
+ ```
171
+
172
+ Supported: `cursor`, `claude-code`, `claude-desktop`, `vscode`, `windsurf`, `codex`.
173
+
174
+ ### Server Lifecycle
175
+
176
+ | Mode | Behavior |
177
+ |------|----------|
178
+ | `lazy` (default) | Connect on first tool call. Disconnect after idle timeout. Cached metadata keeps search/list working offline. |
179
+ | `eager` | Connect at startup. No auto-reconnect on drop. |
180
+ | `keep-alive` | Connect at startup. Auto-reconnect via health checks. |
181
+
182
+ ### Usage Reference
183
+
184
+ | Command | Description |
185
+ |---------|-------------|
186
+ | `mcp({ })` | Show server status |
187
+ | `mcp({ server: "name" })` | List tools from a server |
188
+ | `mcp({ search: "query" })` | Search tools (space-separated words OR'd) |
189
+ | `mcp({ describe: "tool_name" })` | Show tool parameters |
190
+ | `mcp({ tool: "name", args: '{}' })` | Call a tool (args is a JSON string) |
191
+ | `mcp({ connect: "name" })` | Force connect/reconnect a server |
192
+ | `/mcp` | Interactive panel (status, tools, reconnect) |
193
+
194
+ ### Known Limitations
195
+
196
+ - **OAuth servers require `mcp-remote`**: The adapter doesn't implement the MCP OAuth browser flow natively. Use `mcp-remote` as a stdio proxy for OAuth servers.
197
+ - **Figma remote MCP** (`mcp.figma.com`): Blocks dynamic client registration — only whitelisted clients can connect via OAuth. Use Figma's desktop app local MCP server instead (`http://127.0.0.1:3845/mcp`), which requires Dev Mode (paid plan).
198
+ - **Metadata cache**: `pi-mcp-adapter` caches tool metadata to `~/.pi/agent/mcp-cache.json` (hardcoded path, doesn't affect functionality).
199
+ - **OAuth token storage**: `mcp-remote` stores tokens in `~/.mcp-auth/`, separate from Kata's config dir.
200
+
67
201
  ## The /kata Command
68
202
 
69
203
  The main extension registers `/kata` with subcommands:
@@ -111,8 +245,9 @@ Kata uses `~/.kata-cli/` (not `~/.kata/`) to avoid collision with other Kata app
111
245
  agents/ — Synced from src/resources/agents/
112
246
  skills/ — Synced from src/resources/skills/
113
247
  AGENTS.md — Synced from src/resources/AGENTS.md
248
+ mcp.json — MCP server configuration (scaffolded on first launch, never overwritten)
114
249
  auth.json — API keys
115
- settings.json — User settings
250
+ settings.json — User settings (includes packages: ["npm:pi-mcp-adapter"])
116
251
  models.json — Custom model definitions
117
252
  sessions/ — Session history
118
253
  preferences.md — Global Kata preferences
@@ -130,6 +265,7 @@ Set by `loader.ts` before pi starts:
130
265
  | `KATA_BIN_PATH` | Absolute path to loader, used by subagent |
131
266
  | `KATA_WORKFLOW_PATH` | Absolute path to bundled KATA-WORKFLOW.md |
132
267
  | `KATA_BUNDLED_EXTENSION_PATHS` | Colon-joined extension entry points for subagent |
268
+ | `KATA_MCP_CONFIG_PATH` | Absolute path to `~/.kata-cli/agent/mcp.json` |
133
269
 
134
270
  ## Development
135
271
 
@@ -143,7 +279,7 @@ npm run copy-themes
143
279
  # Run
144
280
  node dist/loader.js
145
281
 
146
- # Test
282
+ # Test (37 tests: app smoke, resource sync, MCP integration, package validation)
147
283
  npm test
148
284
  ```
149
285
 
package/dist/cli.js CHANGED
@@ -36,10 +36,30 @@ if (!settingsManager.getQuietStartup()) {
36
36
  if (!settingsManager.getCollapseChangelog()) {
37
37
  settingsManager.setCollapseChangelog(true);
38
38
  }
39
+ // Ensure pi-mcp-adapter is in the packages list so pi auto-installs it on startup.
40
+ // Bootstrap only when packages have never been configured. If users later remove the
41
+ // adapter from settings.json, that opt-out should persist.
42
+ const MCP_ADAPTER_PACKAGE = 'npm:pi-mcp-adapter';
43
+ const globalSettings = settingsManager.getGlobalSettings();
44
+ const globalPackages = [...(globalSettings.packages ?? [])];
45
+ const hasConfiguredPackages = Object.prototype.hasOwnProperty.call(globalSettings, "packages");
46
+ if (!hasConfiguredPackages && !globalPackages.includes(MCP_ADAPTER_PACKAGE)) {
47
+ settingsManager.setPackages([...globalPackages, MCP_ADAPTER_PACKAGE]);
48
+ }
49
+ await settingsManager.flush();
39
50
  const sessionManager = SessionManager.create(process.cwd(), sessionsDir);
40
51
  initResources(agentDir);
41
52
  const resourceLoader = buildResourceLoader(agentDir);
42
53
  await resourceLoader.reload();
54
+ // Inject --mcp-config flag value into the extension runtime.
55
+ // pi-mcp-adapter reads this via pi.getFlag("mcp-config") at session_start.
56
+ // Kata doesn't call pi's main() which does the two-pass argv parsing that
57
+ // normally populates flagValues, so we must do it manually here.
58
+ const mcpConfigPath = process.env.KATA_MCP_CONFIG_PATH;
59
+ if (mcpConfigPath) {
60
+ const extResult = resourceLoader.getExtensions();
61
+ extResult.runtime.flagValues.set('mcp-config', mcpConfigPath);
62
+ }
43
63
  const { session, extensionsResult } = await createAgentSession({
44
64
  authStorage,
45
65
  modelRegistry,
package/dist/loader.js CHANGED
@@ -91,5 +91,15 @@ process.env.KATA_BUNDLED_EXTENSION_PATHS = [
91
91
  join(agentDir, "extensions", "ask-user-questions.ts"),
92
92
  join(agentDir, "extensions", "get-secrets-from-user.ts"),
93
93
  ].join(":");
94
+ // KATA_MCP_CONFIG_PATH — absolute path to Kata's MCP config file.
95
+ // pi-mcp-adapter reads --mcp-config from process.argv directly (before session_start fires).
96
+ // We inject it here so the adapter uses ~/.kata-cli/agent/mcp.json instead of the
97
+ // default ~/.pi/agent/mcp.json.
98
+ const mcpConfigPath = join(agentDir, "mcp.json");
99
+ process.env.KATA_MCP_CONFIG_PATH = mcpConfigPath;
100
+ const hasMcpConfigArg = process.argv.some((arg) => arg === "--mcp-config" || arg.startsWith("--mcp-config="));
101
+ if (!hasMcpConfigArg) {
102
+ process.argv.push("--mcp-config", mcpConfigPath);
103
+ }
94
104
  // Dynamic import defers ESM evaluation — config.js will see PI_PACKAGE_DIR above
95
105
  await import("./cli.js");
@@ -2,6 +2,19 @@ import { DefaultResourceLoader } from '@mariozechner/pi-coding-agent';
2
2
  import { cpSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
3
3
  import { dirname, join, resolve } from 'node:path';
4
4
  import { fileURLToPath } from 'node:url';
5
+ /**
6
+ * Starter mcp.json written to agentDir on first launch.
7
+ * Uses the `imports` field so users can pull in their existing Claude/Cursor config.
8
+ * mcpServers is intentionally empty — users add their own servers here.
9
+ */
10
+ const STARTER_MCP_JSON = JSON.stringify({
11
+ imports: [],
12
+ settings: {
13
+ toolPrefix: 'server',
14
+ idleTimeout: 10,
15
+ },
16
+ mcpServers: {},
17
+ }, null, 2) + '\n';
5
18
  // Resolves to the bundled src/resources/ inside the npm package at runtime:
6
19
  // dist/resource-loader.js → .. → package root → src/resources/
7
20
  const resourcesDir = resolve(dirname(fileURLToPath(import.meta.url)), '..', 'src', 'resources');
@@ -39,6 +52,12 @@ export function initResources(agentDir) {
39
52
  if (existsSync(srcAgentsMd)) {
40
53
  writeFileSync(destAgentsMd, readFileSync(srcAgentsMd));
41
54
  }
55
+ // Scaffold starter mcp.json — only if it doesn't exist yet.
56
+ // Never overwrite: preserve the user's MCP server configuration.
57
+ const mcpConfigPath = join(agentDir, 'mcp.json');
58
+ if (!existsSync(mcpConfigPath)) {
59
+ writeFileSync(mcpConfigPath, STARTER_MCP_JSON, 'utf-8');
60
+ }
42
61
  }
43
62
  /**
44
63
  * Constructs a DefaultResourceLoader with no additionalExtensionPaths.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kata-sh/cli",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
4
4
  "description": "Kata CLI coding agent",
5
5
  "license": "MIT",
6
6
  "private": false,
@@ -9,7 +9,7 @@ Kata CLI is a thin wrapper around pi-coding-agent that provides:
9
9
  - **Branded entry point**: `src/loader.ts` sets env vars and launches `src/cli.ts`
10
10
  - **Bundled extensions**: `src/resources/extensions/` contains all built-in extensions
11
11
  - **Resource syncing**: `src/resource-loader.ts` copies bundled extensions to `~/.kata-cli/agent/` on startup
12
- - **Config directory**: `~/.kata-cli/` (not `~/.kata-cli/` to avoid collision with other Kata apps)
12
+ - **Config directory**: `~/.kata-cli/` (not `~/.pi/` to avoid collision with other Kata apps)
13
13
  - **Package shim**: `pkg/package.json` provides `piConfig` with `name: "kata"` and `configDir: ".kata-cli"`
14
14
 
15
15
  ## Directory Structure
@@ -18,7 +18,7 @@ Kata CLI is a thin wrapper around pi-coding-agent that provides:
18
18
  apps/cli/
19
19
  src/
20
20
  loader.ts — Entry point, sets KATA_* env vars, imports cli.ts
21
- cli.ts — Thin wrapper that calls pi-coding-agent's main()
21
+ cli.ts — Thin wrapper that calls createAgentSession() + InteractiveMode
22
22
  app-paths.ts — Exports appRoot, agentDir, sessionsDir, authFilePath
23
23
  resource-loader.ts — Syncs bundled resources to ~/.kata-cli/agent/
24
24
  wizard.ts — First-run setup, env key hydration
@@ -55,6 +55,7 @@ Kata sets these env vars in `loader.ts` before importing `cli.ts`:
55
55
  | `KATA_BIN_PATH` | Absolute path to loader, used by subagent to spawn Kata |
56
56
  | `KATA_WORKFLOW_PATH` | Absolute path to bundled KATA-WORKFLOW.md |
57
57
  | `KATA_BUNDLED_EXTENSION_PATHS` | Colon-joined list of extension entry points |
58
+ | `KATA_MCP_CONFIG_PATH` | Absolute path to `~/.kata-cli/agent/mcp.json` (also injected as `--mcp-config` argv) |
58
59
 
59
60
  ## The /kata Command
60
61
 
@@ -99,6 +100,156 @@ Kata stores project state in `.kata/` at the project root:
99
100
  - **Copy themes**: `npm run copy-themes` (copies theme assets from pi-coding-agent)
100
101
  - **Dependencies**: Consumed via npm from `@mariozechner/pi-coding-agent` — never fork
101
102
 
103
+ ## MCP Support
104
+
105
+ Kata ships with MCP (Model Context Protocol) support via [`pi-mcp-adapter`](https://github.com/nicobailon/pi-mcp-adapter), auto-installed on first launch. One proxy `mcp` tool (~200 tokens) gives the agent on-demand access to any MCP server's tools without burning context on tool definitions.
106
+
107
+ ### How it works
108
+
109
+ There are three integration points that make MCP work in Kata:
110
+
111
+ 1. **Package seeding** (`cli.ts`): Seeds `npm:pi-mcp-adapter` into `settingsManager.getPackages()` on every startup. Pi's package manager auto-installs it globally if missing.
112
+
113
+ 2. **Config path injection** (`loader.ts` + `cli.ts`): Kata bypasses pi's `main()` and calls `createAgentSession()` directly, which means pi's two-pass argv parsing (that normally populates `runtime.flagValues`) never runs. Two things compensate:
114
+ - `loader.ts` pushes `--mcp-config ~/.kata-cli/agent/mcp.json` into `process.argv` — the adapter reads this at extension load time for `directTools` registration.
115
+ - `cli.ts` manually sets `runtime.flagValues.set('mcp-config', ...)` after `resourceLoader.reload()` — the adapter reads this via `pi.getFlag('mcp-config')` at `session_start` for the main initialization.
116
+
117
+ 3. **Config scaffolding** (`resource-loader.ts`): Creates a starter `~/.kata-cli/agent/mcp.json` on first launch. Never overwrites existing config.
118
+
119
+ ### Configuring MCP servers
120
+
121
+ Edit `~/.kata-cli/agent/mcp.json` to add servers. Servers can use **stdio** (local process) or **HTTP** (remote endpoint) transport.
122
+
123
+ #### Stdio servers (local process)
124
+
125
+ Most MCP servers run as a local process via `npx`:
126
+
127
+ ```json
128
+ {
129
+ "mcpServers": {
130
+ "my-server": {
131
+ "command": "npx",
132
+ "args": ["-y", "some-mcp-server"],
133
+ "env": {
134
+ "API_KEY": "${MY_API_KEY}"
135
+ }
136
+ }
137
+ }
138
+ }
139
+ ```
140
+
141
+ Environment variables support `${VAR}` interpolation from `process.env`.
142
+
143
+ #### HTTP servers with OAuth (e.g. Linear)
144
+
145
+ Many hosted MCP servers (Linear, Figma, etc.) use OAuth 2.1 authentication via the MCP spec. These require [`mcp-remote`](https://github.com/geelen/mcp-remote) as a stdio proxy that handles the OAuth browser flow:
146
+
147
+ ```json
148
+ {
149
+ "mcpServers": {
150
+ "linear": {
151
+ "command": "npx",
152
+ "args": ["-y", "mcp-remote", "https://mcp.linear.app/mcp"]
153
+ }
154
+ }
155
+ }
156
+ ```
157
+
158
+ On first connection, `mcp-remote` opens a browser window for OAuth consent. Tokens are cached in `~/.mcp-auth/` for subsequent sessions.
159
+
160
+ **Linear MCP setup (complete example):**
161
+
162
+ 1. Add the server to `~/.kata-cli/agent/mcp.json`:
163
+ ```json
164
+ {
165
+ "settings": { "toolPrefix": "server", "idleTimeout": 10 },
166
+ "mcpServers": {
167
+ "linear": {
168
+ "command": "npx",
169
+ "args": ["-y", "mcp-remote", "https://mcp.linear.app/mcp"]
170
+ }
171
+ }
172
+ }
173
+ ```
174
+
175
+ 2. Restart Kata.
176
+
177
+ 3. Connect the server (triggers the OAuth flow in your browser):
178
+ ```
179
+ mcp({ connect: "linear" })
180
+ ```
181
+
182
+ 4. Authorize Kata in the browser when prompted by Linear.
183
+
184
+ 5. Use Linear tools:
185
+ ```
186
+ mcp({ server: "linear" }) — list all Linear tools
187
+ mcp({ search: "issues" }) — search for issue-related tools
188
+ mcp({ tool: "linear_list_teams" }) — call a specific tool
189
+ ```
190
+
191
+ **Troubleshooting OAuth:**
192
+ - If you see `internal server error`, clear cached auth: `rm -rf ~/.mcp-auth` and reconnect.
193
+ - Make sure you're running a recent version of Node.js.
194
+ - Use `/mcp` to check server status interactively.
195
+
196
+ #### HTTP servers with bearer token auth
197
+
198
+ For servers that accept API keys or personal access tokens:
199
+
200
+ ```json
201
+ {
202
+ "mcpServers": {
203
+ "my-api": {
204
+ "url": "https://api.example.com/mcp",
205
+ "auth": "bearer",
206
+ "bearerTokenEnv": "MY_API_KEY"
207
+ }
208
+ }
209
+ }
210
+ ```
211
+
212
+ #### Importing existing configs
213
+
214
+ Pull in your existing Claude Code, Cursor, or VS Code MCP configuration:
215
+
216
+ ```json
217
+ {
218
+ "imports": ["claude-code", "cursor"],
219
+ "mcpServers": {}
220
+ }
221
+ ```
222
+
223
+ Supported sources: `cursor`, `claude-code`, `claude-desktop`, `vscode`, `windsurf`, `codex`.
224
+
225
+ ### Server lifecycle
226
+
227
+ | Mode | Behavior |
228
+ |------|----------|
229
+ | `lazy` (default) | Connect on first tool call. Disconnect after idle timeout. Cached metadata keeps search/list working offline. |
230
+ | `eager` | Connect at startup. No auto-reconnect on drop. |
231
+ | `keep-alive` | Connect at startup. Auto-reconnect via health checks. |
232
+
233
+ ### Usage
234
+
235
+ ```
236
+ mcp({ }) — show server status
237
+ mcp({ server: "linear" }) — list tools from a server
238
+ mcp({ search: "issues create" }) — search tools (space-separated words OR'd)
239
+ mcp({ describe: "linear_save_issue" }) — show tool parameters
240
+ mcp({ tool: "linear_list_teams" }) — call a tool (no args)
241
+ mcp({ tool: "linear_save_issue", args: '{"title": "Bug fix"}' }) — call with args (JSON string)
242
+ mcp({ connect: "linear" }) — force connect/reconnect a server
243
+ /mcp — interactive panel (status, tools, reconnect, OAuth)
244
+ ```
245
+
246
+ ### Known limitations
247
+
248
+ - **OAuth servers require `mcp-remote`**: The adapter doesn't implement the MCP OAuth browser flow natively. Use `mcp-remote` as a stdio proxy for any server that requires OAuth (Linear, Figma remote, etc.).
249
+ - **Figma remote MCP (`mcp.figma.com`)**: Blocks dynamic client registration — only whitelisted clients (Cursor, Claude Code, VS Code) can connect via OAuth. Use the Figma desktop app's local MCP server instead (`http://127.0.0.1:3845/mcp`), which requires Figma desktop with Dev Mode (paid plan).
250
+ - **Metadata cache path**: `pi-mcp-adapter` caches tool metadata to `~/.pi/agent/mcp-cache.json` (hardcoded). This doesn't affect functionality — just means the cache lives outside Kata's config dir.
251
+ - **OAuth token storage**: `mcp-remote` stores tokens in `~/.mcp-auth/`, separate from Kata's config dir.
252
+
102
253
  ## Key Conventions
103
254
 
104
255
  - All env var names use `KATA_` prefix (not `GSD_` or `PI_`)
@@ -106,3 +257,4 @@ Kata stores project state in `.kata/` at the project root:
106
257
  - Extensions are synced from `src/resources/extensions/` to `~/.kata-cli/agent/extensions/` on every launch
107
258
  - The `shared/` extension directory is a library, not an entry point — it's imported by other extensions
108
259
  - Branch naming for workflow: `kata/M001/S01` (milestone/slice)
260
+ - MCP config lives at `~/.kata-cli/agent/mcp.json` (not `~/.pi/agent/mcp.json`)
@@ -398,8 +398,9 @@ function parseFrontmatterBlock(frontmatter: string): KataPreferences {
398
398
  const valuePart = remainder.trim();
399
399
 
400
400
  if (valuePart === "") {
401
- const nextLine = lines[i + 1] ?? "";
402
- const nextTrimmed = nextLine.trim();
401
+ const nextNonEmptyLine =
402
+ lines.slice(i + 1).find((candidate) => candidate.trim() !== "") ?? "";
403
+ const nextTrimmed = nextNonEmptyLine.trim();
403
404
  if (nextTrimmed.startsWith("- ")) {
404
405
  const items: unknown[] = [];
405
406
  let j = i + 1;
@@ -481,9 +482,16 @@ function parseFrontmatterBlock(frontmatter: string): KataPreferences {
481
482
  current[key] = items;
482
483
  i = j - 1;
483
484
  } else {
484
- const obj: Record<string, unknown> = {};
485
- current[key] = obj;
486
- stack.push({ indent, value: obj });
485
+ // Check if the next non-empty line is actually indented deeper (a real nested block).
486
+ // If not, this key simply has no value — skip it rather than creating an empty object.
487
+ const nextIndent =
488
+ nextNonEmptyLine.match(/^\s*/)?.[0].length ?? indent;
489
+ if (nextIndent > indent) {
490
+ const obj: Record<string, unknown> = {};
491
+ current[key] = obj;
492
+ stack.push({ indent, value: obj });
493
+ }
494
+ // else: key with no value and no nested block — leave it undefined
487
495
  }
488
496
  continue;
489
497
  }
@@ -494,9 +502,13 @@ function parseFrontmatterBlock(frontmatter: string): KataPreferences {
494
502
  return root as KataPreferences;
495
503
  }
496
504
 
497
- function parseScalar(value: string): string | number | boolean {
505
+ function parseScalar(
506
+ value: string,
507
+ ): string | number | boolean | unknown[] | Record<string, never> {
498
508
  if (value === "true") return true;
499
509
  if (value === "false") return false;
510
+ if (value === "[]") return [];
511
+ if (value === "{}") return {};
500
512
  if (/^-?\d+$/.test(value)) return Number(value);
501
513
  return value.replace(/^['\"]|['\"]$/g, "");
502
514
  }
@@ -0,0 +1,53 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { execFileSync } from 'node:child_process';
4
+ import { mkdirSync, mkdtempSync, writeFileSync } from 'node:fs';
5
+ import { tmpdir } from 'node:os';
6
+ import { join } from 'node:path';
7
+ import { fileURLToPath } from 'node:url';
8
+
9
+ const resolveTsHookPath = fileURLToPath(new URL('./resolve-ts.mjs', import.meta.url));
10
+ const preferencesPath = fileURLToPath(new URL('../preferences.ts', import.meta.url));
11
+
12
+ test('loadEffectiveKataPreferences preserves blank-line-separated skill_rules lists', () => {
13
+ const tmp = mkdtempSync(join(tmpdir(), 'kata-preferences-frontmatter-'));
14
+ const kataDir = join(tmp, '.kata');
15
+ mkdirSync(kataDir, { recursive: true });
16
+ writeFileSync(
17
+ join(kataDir, 'preferences.md'),
18
+ `---
19
+ skill_rules:
20
+
21
+ - when: build
22
+ use:
23
+ - test-driven-development
24
+ ---
25
+ `,
26
+ );
27
+
28
+ const script = `
29
+ import { loadEffectiveKataPreferences } from ${JSON.stringify(preferencesPath)};
30
+ const prefs = loadEffectiveKataPreferences();
31
+ console.log(JSON.stringify(prefs?.preferences.skill_rules ?? null));
32
+ `;
33
+
34
+ const output = execFileSync(
35
+ 'node',
36
+ ['--import', resolveTsHookPath, '--experimental-strip-types', '-e', script],
37
+ {
38
+ cwd: tmp,
39
+ env: {
40
+ ...process.env,
41
+ HOME: tmp,
42
+ },
43
+ encoding: 'utf-8',
44
+ },
45
+ ).trim();
46
+
47
+ assert.deepEqual(JSON.parse(output), [
48
+ {
49
+ when: 'build',
50
+ use: ['test-driven-development'],
51
+ },
52
+ ]);
53
+ });