pi-mcp-adapter 2.4.0 → 2.4.2

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/CHANGELOG.md CHANGED
@@ -7,6 +7,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [2.4.2] - 2026-04-22
11
+
12
+ ### Fixed
13
+ - Migrated extension tool schemas from `@sinclair/typebox` to `typebox` 1.x so packaged installs follow Pi's current extension runtime contract.
14
+
15
+ ### Changed
16
+ - Replaced the legacy `@sinclair/typebox` runtime dependency with `typebox`.
17
+
18
+ ## [2.4.1] - 2026-04-22
19
+
20
+ ### Added
21
+ - Added standard-MCP-first config discovery: `~/.config/mcp/mcp.json` and project `.mcp.json` now load automatically, with Pi-owned files preserved as override layers.
22
+ - Added `pi-mcp-adapter init` as a native post-install helper that detects host-specific MCP configs and scaffolds Pi compatibility imports without using the old raw GitHub downloader flow.
23
+ - Added first-run onboarding inside the extension: `/mcp` now shows shared-config hints or actionable empty states, and `/mcp setup` opens a guided setup flow for compatibility imports, minimal `.mcp.json` scaffolding, detected config paths, RepoPrompt quick-add, and exact before/after write previews.
24
+ - Added automatic Pi-core reload after setup or direct-tool config changes, using the same flow as `/reload` so freshly configured direct tools can appear without a manual restart.
25
+ - Added a dedicated Pi-owned onboarding state file so shared-config hints behave as one-time guidance instead of repeating every session.
26
+
27
+ ### Changed
28
+ - Updated config precedence to prefer shared MCP files first, then Pi overrides, with `.pi/mcp.json` acting as the final Pi-specific project override.
29
+ - Updated Claude Code compatibility probing to prefer modern Claude MCP config locations before legacy paths.
30
+ - Updated project scaffolding so generated `.mcp.json` files are safe minimal shells instead of fake placeholder servers that fail on first reload.
31
+ - Updated the setup panel and README for clearer first-run guidance, improved spacing, and a more digestible shared-MCP-first setup story.
32
+
10
33
  ## [2.4.0] - 2026-04-13
11
34
 
12
35
  ### Added
package/README.md CHANGED
@@ -24,9 +24,21 @@ pi install npm:pi-mcp-adapter
24
24
 
25
25
  Restart Pi after installation.
26
26
 
27
+ ## What happens on first run
28
+
29
+ The adapter reads standard MCP files automatically. No extra setup needed if you already have them.
30
+
31
+ | You already have... | What happens |
32
+ |---------------------|--------------|
33
+ | `.mcp.json` or `~/.config/mcp/mcp.json` | Pi uses it immediately. The first time you open `/mcp`, you'll see a short heads-up explaining which file Pi detected and that Pi only writes adapter-specific overrides to its own files. |
34
+ | Host-specific configs (Cursor, Claude Code, Codex, etc.) but no standard MCP files | Run `/mcp setup` to adopt those host configs into Pi. The setup flow shows exactly what it found, lets you pick which ones to import, and previews the exact file changes before writing. |
35
+ | Nothing configured yet | Run `/mcp setup` to scaffold a minimal `.mcp.json`, quick-add RepoPrompt, or inspect what the adapter discovered on your machine. |
36
+
37
+ If you prefer the terminal, you can also run `pi-mcp-adapter init` after install to scan for host-specific configs and add missing compatibility imports to `~/.pi/agent/mcp.json`.
38
+
27
39
  ## Quick Start
28
40
 
29
- Create `~/.pi/agent/mcp.json`:
41
+ Preferred project config: `.mcp.json`
30
42
 
31
43
  ```json
32
44
  {
@@ -39,6 +51,20 @@ Create `~/.pi/agent/mcp.json`:
39
51
  }
40
52
  ```
41
53
 
54
+ Preferred user-global shared config: `~/.config/mcp/mcp.json`
55
+
56
+ Pi also reads Pi-owned override files for settings and host-specific compatibility:
57
+
58
+ - `~/.pi/agent/mcp.json` — Pi global override
59
+ - `.pi/mcp.json` — Pi project override
60
+
61
+ Precedence is:
62
+
63
+ 1. `~/.config/mcp/mcp.json`
64
+ 2. `~/.pi/agent/mcp.json`
65
+ 3. `.mcp.json`
66
+ 4. `.pi/mcp.json`
67
+
42
68
  Servers are **lazy by default** — they won't connect until you actually call one of their tools. The adapter caches tool metadata so search and describe work without live connections.
43
69
 
44
70
  ```
@@ -62,6 +88,19 @@ Two calls instead of 26 tools cluttering the context.
62
88
 
63
89
  ## Config
64
90
 
91
+ ### File Layout
92
+
93
+ Use the shared MCP files when you want one setup to work across hosts, and Pi-owned files when you need Pi-specific overrides or settings.
94
+
95
+ | File | Purpose |
96
+ |------|---------|
97
+ | `~/.config/mcp/mcp.json` | User-global shared MCP config |
98
+ | `.mcp.json` | Project-local shared MCP config |
99
+ | `~/.pi/agent/mcp.json` | Pi global override and compatibility imports |
100
+ | `.pi/mcp.json` | Pi project override |
101
+
102
+ Pi-specific files are the write targets for imported or shared global servers when Pi needs to persist adapter-only settings such as `directTools`.
103
+
65
104
  ### Server Options
66
105
 
67
106
  ```json
@@ -190,9 +229,13 @@ To exclude specific tools while still using `directTools: true`, add `excludeToo
190
229
 
191
230
  Each direct tool costs ~150-300 tokens in the system prompt (name + description + schema). Good for targeted sets of 5-20 tools. For servers with 75+ tools, stick with the proxy or pick specific tools with a `string[]`.
192
231
 
193
- Direct tools register from the metadata cache (`~/.pi/agent/mcp-cache.json`), so no server connections are needed at startup. On the first session after adding `directTools` to a new server, the cache won't exist yet — tools fall back to proxy-only and the cache populates in the background. Restart Pi and they'll be available. To force it: `/mcp reconnect <server>` then restart.
232
+ Direct tools register from the metadata cache (`~/.pi/agent/mcp-cache.json`), so no server connections are needed at startup. On the first session after adding `directTools` to a new server, the cache won't exist yet — tools fall back to proxy-only and the cache populates in the background. To force it: `/mcp reconnect <server>`.
233
+
234
+ When you change direct-tool toggles in `/mcp` or write new config through `/mcp setup`, the extension triggers Pi's normal reload flow automatically. That refreshes extensions, prompts, skills, and MCP tool registration in one shot, so newly configured direct tools can appear without a manual restart.
194
235
 
195
- **Interactive configuration:** Run `/mcp` to open an interactive panel showing all servers with connection status, tools, and direct/proxy toggles. You can reconnect servers, initiate OAuth, and toggle tools between direct and proxy — all from one overlay. Changes are written to your config file; restart Pi to apply.
236
+ **Interactive configuration:** Run `/mcp` to open an interactive panel showing all servers with connection status, tools, and direct/proxy toggles. You can reconnect servers, initiate OAuth, and toggle tools between direct and proxy — all from one overlay.
237
+
238
+ **Guided first-run setup:** Run `/mcp setup` to inspect detected shared MCP files, adopt compatibility imports from other hosts, open discovered config paths, preview exact before/after file diffs for writes, scaffold a minimal project `.mcp.json`, or quick-add RepoPrompt into a standard/shared MCP file.
196
239
 
197
240
  **Subagent integration:** If you use the subagent extension, agents can request direct MCP tools in their frontmatter with `mcp:server-name` syntax. See the subagent README for details.
198
241
 
@@ -257,7 +300,7 @@ Restart pi, then ask the agent to show a chart — it calls `show_chart` and ope
257
300
 
258
301
  ### Import Existing Configs
259
302
 
260
- Already have MCP set up elsewhere? Import it:
303
+ Shared MCP files are loaded automatically. Use `imports` only for host-specific config formats that are not already covered by `.mcp.json` or `~/.config/mcp/mcp.json`.
261
304
 
262
305
  ```json
263
306
  {
@@ -266,11 +309,13 @@ Already have MCP set up elsewhere? Import it:
266
309
  }
267
310
  ```
268
311
 
269
- Supported: `cursor`, `claude-code`, `claude-desktop`, `vscode`, `windsurf`, `codex`
312
+ Supported compatibility imports: `cursor`, `claude-code`, `claude-desktop`, `vscode`, `windsurf`, `codex`
313
+
314
+ `pi-mcp-adapter init` detects these host-specific configs and adds missing imports to `~/.pi/agent/mcp.json` for you.
270
315
 
271
316
  ### Project Config
272
317
 
273
- Add `.pi/mcp.json` in a project root for project-specific servers. Project config overrides global and imported servers.
318
+ Prefer `.mcp.json` for project-local shared MCP config. Use `.pi/mcp.json` only when you need a Pi-specific project override. Project files override both user-global shared MCP config and Pi global overrides.
274
319
 
275
320
  ## Usage
276
321
 
@@ -292,7 +337,8 @@ Tool names are fuzzy-matched on hyphens and underscores — `context7_resolve_li
292
337
 
293
338
  | Command | What it does |
294
339
  |---------|--------------|
295
- | `/mcp` | Interactive panel (server status, tool toggles, reconnect) |
340
+ | `/mcp` | Interactive panel and first-run onboarding surface |
341
+ | `/mcp setup` | Guided setup for imports, a minimal `.mcp.json`, RepoPrompt quick-add, and config-path inspection |
296
342
  | `/mcp tools` | List all tools |
297
343
  | `/mcp reconnect` | Reconnect all servers |
298
344
  | `/mcp reconnect <server>` | Connect or reconnect a single server |
package/cli.js CHANGED
@@ -2,107 +2,173 @@
2
2
 
3
3
  import fs from "node:fs";
4
4
  import path from "node:path";
5
- import https from "node:https";
6
5
  import os from "node:os";
7
- import { execSync } from "node:child_process";
8
-
9
- const REPO_URL = "https://raw.githubusercontent.com/nicobailon/pi-mcp-adapter/main";
10
- const EXT_DIR = path.join(os.homedir(), ".pi", "agent", "extensions", "pi-mcp-adapter");
11
- const SETTINGS_FILE = path.join(os.homedir(), ".pi", "agent", "settings.json");
12
- const EXT_PATH = "~/.pi/agent/extensions/pi-mcp-adapter/index.ts";
13
-
14
- const FILES = [
15
- "index.ts",
16
- "types.ts",
17
- "config.ts",
18
- "server-manager.ts",
19
- "tool-registrar.ts",
20
- "resource-tools.ts",
21
- "lifecycle.ts",
22
- "metadata-cache.ts",
23
- "npx-resolver.ts",
24
- "oauth-handler.ts",
25
- "package.json",
26
- "tsconfig.json",
27
- "README.md",
28
- "CHANGELOG.md",
29
- "LICENSE",
30
- ];
31
-
32
- function download(url) {
33
- return new Promise((resolve, reject) => {
34
- https.get(url, (res) => {
35
- if (res.statusCode === 301 || res.statusCode === 302) {
36
- return download(res.headers.location).then(resolve).catch(reject);
37
- }
38
- if (res.statusCode !== 200) {
39
- return reject(new Error(`Failed to download ${url}: ${res.statusCode}`));
40
- }
41
- let data = "";
42
- res.on("data", (chunk) => (data += chunk));
43
- res.on("end", () => resolve(data));
44
- res.on("error", reject);
45
- }).on("error", reject);
46
- });
47
- }
6
+ import { pathToFileURL } from "node:url";
7
+
8
+ const HOME = os.homedir();
9
+ const PI_CONFIG_PATH = path.join(HOME, ".pi", "agent", "mcp.json");
10
+ const GENERIC_GLOBAL_CONFIG_PATH = path.join(HOME, ".config", "mcp", "mcp.json");
11
+ const PROJECT_CONFIG_PATH = path.resolve(process.cwd(), ".mcp.json");
12
+ const PROJECT_PI_CONFIG_PATH = path.resolve(process.cwd(), ".pi", "mcp.json");
48
13
 
49
- async function main() {
50
- console.log("Installing pi-mcp-adapter...\n");
14
+ const IMPORT_PATHS = {
15
+ cursor: [path.join(HOME, ".cursor", "mcp.json")],
16
+ "claude-code": [
17
+ path.join(HOME, ".claude", "mcp.json"),
18
+ path.join(HOME, ".claude.json"),
19
+ path.join(HOME, ".claude", "claude_desktop_config.json"),
20
+ ],
21
+ "claude-desktop": [path.join(HOME, "Library", "Application Support", "Claude", "claude_desktop_config.json")],
22
+ codex: [path.join(HOME, ".codex", "config.json")],
23
+ windsurf: [path.join(HOME, ".windsurf", "mcp.json")],
24
+ vscode: [path.resolve(process.cwd(), ".vscode", "mcp.json")],
25
+ };
26
+
27
+ function printHelp(log = console.log) {
28
+ log("pi-mcp-adapter helper\n");
29
+ log("Install the package with:");
30
+ log(" pi install npm:pi-mcp-adapter\n");
31
+ log("Then optionally run:");
32
+ log(" pi-mcp-adapter init Detect host configs and scaffold Pi imports");
33
+ log(" pi-mcp-adapter init --dry-run");
34
+ }
51
35
 
52
- fs.mkdirSync(EXT_DIR, { recursive: true });
53
- console.log(`Created directory: ${EXT_DIR}`);
36
+ function readJsonFile(filePath) {
37
+ return JSON.parse(fs.readFileSync(filePath, "utf-8"));
38
+ }
54
39
 
55
- for (const file of FILES) {
56
- console.log(`Downloading ${file}...`);
57
- const content = await download(`${REPO_URL}/${file}`);
58
- fs.writeFileSync(path.join(EXT_DIR, file), content);
40
+ function loadPiConfig() {
41
+ if (!fs.existsSync(PI_CONFIG_PATH)) {
42
+ return { mcpServers: {} };
59
43
  }
60
44
 
61
- console.log("\nInstalling dependencies...");
62
- try {
63
- execSync("npm install --omit=dev", { cwd: EXT_DIR, stdio: "inherit" });
64
- } catch {
65
- console.error("Warning: npm install failed. You may need to run it manually.");
45
+ const raw = readJsonFile(PI_CONFIG_PATH);
46
+ const mcpServers = raw.mcpServers ?? raw["mcp-servers"] ?? {};
47
+ if (!mcpServers || typeof mcpServers !== "object" || Array.isArray(mcpServers)) {
48
+ throw new Error(`Invalid MCP config at ${PI_CONFIG_PATH}: expected \"mcpServers\" to be an object`);
66
49
  }
67
50
 
68
- console.log(`\nUpdating settings: ${SETTINGS_FILE}`);
69
-
70
- let settings = {};
71
- if (fs.existsSync(SETTINGS_FILE)) {
72
- try {
73
- settings = JSON.parse(fs.readFileSync(SETTINGS_FILE, "utf-8"));
74
- } catch (err) {
75
- console.error(`Warning: Could not parse existing settings.json: ${err.message}`);
76
- console.error("Creating new settings file...");
51
+ const normalized = { ...raw };
52
+ delete normalized["mcp-servers"];
53
+
54
+ const imports = Array.isArray(raw.imports) ? raw.imports.filter((value) => typeof value === "string") : undefined;
55
+ return {
56
+ ...normalized,
57
+ mcpServers,
58
+ imports,
59
+ };
60
+ }
61
+
62
+ function findAvailableImports() {
63
+ const found = [];
64
+
65
+ for (const [kind, candidates] of Object.entries(IMPORT_PATHS)) {
66
+ const existing = candidates.find((candidate) => fs.existsSync(candidate));
67
+ if (existing) {
68
+ found.push({ kind, path: existing });
77
69
  }
78
70
  }
79
71
 
80
- if (!Array.isArray(settings.extensions)) {
81
- settings.extensions = [];
72
+ return found;
73
+ }
74
+
75
+ function printDiscovery(log, imports) {
76
+ log("Config discovery:\n");
77
+
78
+ const paths = [
79
+ ["User-global standard MCP", GENERIC_GLOBAL_CONFIG_PATH],
80
+ ["Pi global override", PI_CONFIG_PATH],
81
+ ["Project standard MCP", PROJECT_CONFIG_PATH],
82
+ ["Project Pi override", PROJECT_PI_CONFIG_PATH],
83
+ ];
84
+
85
+ for (const [label, filePath] of paths) {
86
+ const prefix = fs.existsSync(filePath) ? "✓" : "-";
87
+ log(`${prefix} ${label}: ${filePath}`);
88
+ }
89
+
90
+ log("\nCompatibility imports:\n");
91
+ if (imports.length === 0) {
92
+ log("- No host-specific MCP configs detected");
93
+ return;
94
+ }
95
+
96
+ for (const entry of imports) {
97
+ log(`✓ ${entry.kind}: ${entry.path}`);
98
+ }
99
+ }
100
+
101
+ function writePiConfig(config) {
102
+ fs.mkdirSync(path.dirname(PI_CONFIG_PATH), { recursive: true });
103
+ fs.writeFileSync(PI_CONFIG_PATH, `${JSON.stringify(config, null, 2)}\n`, "utf-8");
104
+ }
105
+
106
+ async function runInit(argv, log = console.log) {
107
+ const dryRun = argv.includes("--dry-run");
108
+ const foundImports = findAvailableImports();
109
+ const existingConfig = loadPiConfig();
110
+ const existingImports = new Set(existingConfig.imports ?? []);
111
+ const importsToAdd = foundImports
112
+ .map((entry) => entry.kind)
113
+ .filter((kind) => !existingImports.has(kind));
114
+
115
+ printDiscovery(log, foundImports);
116
+
117
+ if (importsToAdd.length === 0) {
118
+ log("\nNo Pi config changes needed.");
119
+ log("Standard MCP configs are discovered automatically, and host-specific imports are already configured or unavailable.");
120
+ return 0;
82
121
  }
83
122
 
84
- const hasMcpExt = settings.extensions.some(p =>
85
- p === EXT_PATH ||
86
- p.includes("/extensions/pi-mcp-adapter/index.ts") ||
87
- p.includes("/extensions/pi-mcp-adapter")
88
- );
89
-
90
- if (!hasMcpExt) {
91
- settings.extensions.push(EXT_PATH);
92
- console.log(`Added "${EXT_PATH}" to extensions array`);
93
- } else {
94
- console.log("Extension already configured in settings.json");
123
+ const nextConfig = {
124
+ ...existingConfig,
125
+ imports: [...existingImports, ...importsToAdd],
126
+ mcpServers: existingConfig.mcpServers ?? {},
127
+ };
128
+
129
+ log(`\nDetected host configs to import into Pi: ${importsToAdd.join(", ")}`);
130
+
131
+ if (dryRun) {
132
+ log(`Dry run: would update ${PI_CONFIG_PATH}`);
133
+ return 0;
95
134
  }
96
135
 
97
- fs.mkdirSync(path.dirname(SETTINGS_FILE), { recursive: true });
98
- fs.writeFileSync(SETTINGS_FILE, JSON.stringify(settings, null, 2) + "\n");
136
+ writePiConfig(nextConfig);
137
+ log(`Updated ${PI_CONFIG_PATH}`);
138
+ log("Pi will now keep reading standard MCP configs automatically, while these imports cover host-specific config formats.");
139
+ return 0;
140
+ }
141
+
142
+ export async function main(argv = process.argv.slice(2), log = console.log, error = console.error) {
143
+ const [command, ...rest] = argv;
99
144
 
100
- console.log("\nInstallation complete!");
101
- console.log("\nCreate ~/.pi/agent/mcp.json to configure MCP servers.");
102
- console.log("Restart pi to load the extension.");
145
+ if (!command || command === "help" || command === "--help" || command === "-h") {
146
+ printHelp(log);
147
+ return 0;
148
+ }
149
+
150
+ if (command === "install") {
151
+ error("The custom downloader has been retired.");
152
+ error("Use `pi install npm:pi-mcp-adapter` instead, then optionally run `pi-mcp-adapter init`.");
153
+ return 1;
154
+ }
155
+
156
+ if (command === "init") {
157
+ return runInit(rest, log);
158
+ }
159
+
160
+ error(`Unknown command: ${command}`);
161
+ printHelp(log);
162
+ return 1;
103
163
  }
104
164
 
105
- main().catch((err) => {
106
- console.error(`\nInstallation failed: ${err.message}`);
107
- process.exit(1);
108
- });
165
+ const isEntrypoint = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
166
+
167
+ if (isEntrypoint) {
168
+ main().then((code) => {
169
+ process.exitCode = code;
170
+ }).catch((err) => {
171
+ console.error(`\nHelper failed: ${err instanceof Error ? err.message : String(err)}`);
172
+ process.exit(1);
173
+ });
174
+ }
package/commands.ts CHANGED
@@ -1,12 +1,24 @@
1
1
  import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
2
2
  import type { McpExtensionState } from "./state.js";
3
- import type { McpConfig, ServerEntry, McpPanelCallbacks, McpPanelResult } from "./types.js";
4
- import { getServerProvenance, writeDirectToolsConfig } from "./config.js";
3
+ import type { McpConfig, ServerEntry, McpPanelCallbacks, McpPanelResult, ImportKind } from "./types.js";
4
+ import {
5
+ ensureCompatibilityImports,
6
+ getMcpDiscoverySummary,
7
+ getServerProvenance,
8
+ previewCompatibilityImports,
9
+ previewSharedServerEntry,
10
+ previewStarterProjectConfig,
11
+ writeDirectToolsConfig,
12
+ writeSharedServerEntry,
13
+ writeStarterProjectConfig,
14
+ } from "./config.js";
5
15
  import { lazyConnect, updateMetadataCache, updateStatusBar, getFailureAgeSeconds } from "./init.js";
6
16
  import { loadMetadataCache } from "./metadata-cache.js";
7
17
  import { buildToolMetadata } from "./tool-metadata.js";
8
18
  import { supportsOAuth, authenticate } from "./mcp-auth-flow.js";
9
19
  import { hasStoredTokens } from "./mcp-auth.js";
20
+ import { loadOnboardingState, markSetupCompleted as persistSetupCompleted, markSharedConfigHintShown } from "./onboarding-state.js";
21
+ import { openPath } from "./utils.js";
10
22
 
11
23
  export async function showStatus(state: McpExtensionState, ctx: ExtensionContext): Promise<void> {
12
24
  if (!ctx.hasUI) return;
@@ -42,6 +54,7 @@ export async function showStatus(state: McpExtensionState, ctx: ExtensionContext
42
54
 
43
55
  if (Object.keys(state.config.mcpServers).length === 0) {
44
56
  lines.push("No MCP servers configured");
57
+ lines.push("Run /mcp setup to adopt imports or scaffold a starter .mcp.json");
45
58
  }
46
59
 
47
60
  ctx.ui.notify(lines.join("\n"), "info");
@@ -153,11 +166,8 @@ export async function authenticateServer(
153
166
  return;
154
167
  }
155
168
 
156
- // Full automatic OAuth flow using SDK
157
169
  try {
158
170
  ctx.ui.setStatus("mcp-auth", `Authenticating ${serverName}...`);
159
-
160
- // Runs the configured OAuth flow (interactive browser or non-interactive client_credentials)
161
171
  const status = await authenticate(serverName, definition.url, definition);
162
172
 
163
173
  if (status === "authenticated") {
@@ -180,15 +190,104 @@ export async function authenticateServer(
180
190
  }
181
191
  }
182
192
 
193
+ export interface PanelFlowResult {
194
+ configChanged: boolean;
195
+ }
196
+
197
+ function buildSharedConfigNoticeLines(configOverridePath?: string): { lines: string[]; fingerprint: string | null } {
198
+ const discovery = getMcpDiscoverySummary(configOverridePath);
199
+ const onboardingState = loadOnboardingState();
200
+ if (!discovery.hasSharedServers || onboardingState.sharedConfigHintShown) {
201
+ return { lines: [], fingerprint: null };
202
+ }
203
+
204
+ const sharedSources = discovery.sources.filter((source) => source.kind === "shared" && source.serverCount > 0);
205
+ const sourceList = sharedSources.map((source) => source.path).join(", ");
206
+ return {
207
+ lines: [
208
+ `Using standard MCP config from ${sourceList}.`,
209
+ "Pi only writes compatibility imports and adapter-specific overrides into Pi-owned files when needed.",
210
+ ],
211
+ fingerprint: discovery.fingerprint,
212
+ };
213
+ }
214
+
215
+ export async function openMcpSetup(
216
+ _state: McpExtensionState,
217
+ pi: ExtensionAPI,
218
+ ctx: ExtensionContext,
219
+ configOverridePath?: string,
220
+ mode: "empty" | "setup" = "setup",
221
+ ): Promise<PanelFlowResult> {
222
+ if (!ctx.hasUI) return { configChanged: false };
223
+
224
+ const discovery = getMcpDiscoverySummary(configOverridePath);
225
+ const onboardingState = loadOnboardingState();
226
+ const { createMcpSetupPanel } = await import("./mcp-setup-panel.js");
227
+ let configChanged = false;
228
+
229
+ const callbacks = {
230
+ previewImports: (imports: ImportKind[]) => previewCompatibilityImports(imports, configOverridePath),
231
+ previewStarterProject: () => previewStarterProjectConfig(),
232
+ previewRepoPrompt: () => {
233
+ const repoPrompt = getMcpDiscoverySummary(configOverridePath).repoPrompt;
234
+ if (!repoPrompt.entry || !repoPrompt.targetPath || !repoPrompt.serverName) return null;
235
+ return previewSharedServerEntry(repoPrompt.targetPath, repoPrompt.serverName, repoPrompt.entry);
236
+ },
237
+ adoptImports: async (imports: ImportKind[]) => {
238
+ const result = ensureCompatibilityImports(imports, configOverridePath);
239
+ if (result.added.length > 0) configChanged = true;
240
+ return result;
241
+ },
242
+ scaffoldProjectConfig: async () => {
243
+ const path = writeStarterProjectConfig();
244
+ configChanged = true;
245
+ return { path };
246
+ },
247
+ addRepoPrompt: async () => {
248
+ const repoPrompt = getMcpDiscoverySummary(configOverridePath).repoPrompt;
249
+ if (!repoPrompt.entry || !repoPrompt.targetPath || !repoPrompt.serverName) {
250
+ throw new Error("RepoPrompt is not available to add from this setup screen.");
251
+ }
252
+ const path = writeSharedServerEntry(repoPrompt.targetPath, repoPrompt.serverName, repoPrompt.entry);
253
+ configChanged = true;
254
+ return { path, serverName: repoPrompt.serverName };
255
+ },
256
+ openPath: async (targetPath: string) => {
257
+ await openPath(pi, targetPath);
258
+ },
259
+ markSetupCompleted: () => {
260
+ persistSetupCompleted(discovery.fingerprint);
261
+ },
262
+ };
263
+
264
+ return new Promise<PanelFlowResult>((resolve) => {
265
+ ctx.ui.custom(
266
+ (tui, _theme, _keybindings, done) => {
267
+ return createMcpSetupPanel(discovery, callbacks, { mode, onboardingState }, tui, () => {
268
+ done();
269
+ resolve({ configChanged });
270
+ });
271
+ },
272
+ { overlay: true, overlayOptions: { anchor: "center", width: 92 } },
273
+ );
274
+ });
275
+ }
276
+
183
277
  export async function openMcpPanel(
184
278
  state: McpExtensionState,
185
279
  pi: ExtensionAPI,
186
280
  ctx: ExtensionContext,
187
281
  configOverridePath?: string,
188
- ): Promise<void> {
282
+ ): Promise<PanelFlowResult> {
283
+ if (Object.keys(state.config.mcpServers).length === 0) {
284
+ return openMcpSetup(state, pi, ctx, configOverridePath, "empty");
285
+ }
286
+
189
287
  const config = state.config;
190
288
  const cache = loadMetadataCache();
191
289
  const provenanceMap = getServerProvenance(pi.getFlag("mcp-config") as string | undefined ?? configOverridePath);
290
+ const { lines: noticeLines, fingerprint } = buildSharedConfigNoticeLines(pi.getFlag("mcp-config") as string | undefined ?? configOverridePath);
192
291
 
193
292
  const callbacks: McpPanelCallbacks = {
194
293
  reconnect: async (serverName: string) => {
@@ -219,20 +318,28 @@ export async function openMcpPanel(
219
318
  };
220
319
 
221
320
  const { createMcpPanel } = await import("./mcp-panel.js");
321
+ let configChanged = false;
222
322
 
223
- return new Promise<void>((resolve) => {
323
+ await new Promise<void>((resolve) => {
224
324
  ctx.ui.custom(
225
325
  (tui, _theme, _keybindings, done) => {
226
326
  return createMcpPanel(config, cache, provenanceMap, callbacks, tui, (result: McpPanelResult) => {
227
327
  if (!result.cancelled && result.changes.size > 0) {
228
328
  writeDirectToolsConfig(result.changes, provenanceMap, config);
229
- ctx.ui.notify("Direct tools updated. Restart pi to apply.", "info");
329
+ configChanged = true;
330
+ ctx.ui.notify("Direct tools updated. Pi will reload after this panel closes.", "info");
230
331
  }
231
332
  done();
232
333
  resolve();
233
- });
334
+ }, { noticeLines });
234
335
  },
235
336
  { overlay: true, overlayOptions: { anchor: "center", width: 82 } },
236
337
  );
237
338
  });
339
+
340
+ if (noticeLines.length > 0 && fingerprint) {
341
+ markSharedConfigHintShown(fingerprint);
342
+ }
343
+
344
+ return { configChanged };
238
345
  }