pi-mcp-adapter 2.5.0 → 2.5.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,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [2.5.2] - 2026-04-29
11
+
12
+ ### Fixed
13
+ - Respected `PI_CODING_AGENT_DIR` for Pi-owned MCP config and state files, including metadata cache, npx cache, onboarding state, OAuth credentials, and `pi-mcp-adapter init` writes.
14
+
15
+ ## [2.5.1] - 2026-04-24
16
+
17
+ ### Fixed
18
+ - Changed OAuth browser callbacks to `http://localhost:<port>/callback` for pre-registered clients such as Slack MCP. Thanks @shenal for PR #53.
19
+
10
20
  ## [2.5.0] - 2026-04-24
11
21
 
12
22
  ### Added
package/README.md CHANGED
@@ -34,7 +34,7 @@ The adapter reads standard MCP files automatically. No extra setup needed if you
34
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
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
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`.
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 the Pi agent dir (`~/.pi/agent/mcp.json` by default, or `$PI_CODING_AGENT_DIR/mcp.json` when set).
38
38
 
39
39
  ## Quick Start
40
40
 
@@ -55,13 +55,13 @@ Preferred user-global shared config: `~/.config/mcp/mcp.json`
55
55
 
56
56
  Pi also reads Pi-owned override files for settings and host-specific compatibility:
57
57
 
58
- - `~/.pi/agent/mcp.json` — Pi global override
58
+ - `<Pi agent dir>/mcp.json` — Pi global override (`~/.pi/agent/mcp.json` by default)
59
59
  - `.pi/mcp.json` — Pi project override
60
60
 
61
61
  Precedence is:
62
62
 
63
63
  1. `~/.config/mcp/mcp.json`
64
- 2. `~/.pi/agent/mcp.json`
64
+ 2. `<Pi agent dir>/mcp.json`
65
65
  3. `.mcp.json`
66
66
  4. `.pi/mcp.json`
67
67
 
@@ -96,7 +96,7 @@ Use the shared MCP files when you want one setup to work across hosts, and Pi-ow
96
96
  |------|---------|
97
97
  | `~/.config/mcp/mcp.json` | User-global shared MCP config |
98
98
  | `.mcp.json` | Project-local shared MCP config |
99
- | `~/.pi/agent/mcp.json` | Pi global override and compatibility imports |
99
+ | `<Pi agent dir>/mcp.json` | Pi global override and compatibility imports (`~/.pi/agent/mcp.json` by default) |
100
100
  | `.pi/mcp.json` | Pi project override |
101
101
 
102
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`.
@@ -231,7 +231,7 @@ To exclude specific tools while still using `directTools: true`, add `excludeToo
231
231
 
232
232
  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[]`.
233
233
 
234
- 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>`.
234
+ Direct tools register from the metadata cache in the Pi agent dir (`~/.pi/agent/mcp-cache.json` by default, or `$PI_CODING_AGENT_DIR/mcp-cache.json` when set), 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>`.
235
235
 
236
236
  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.
237
237
 
@@ -313,7 +313,7 @@ Shared MCP files are loaded automatically. Use `imports` only for host-specific
313
313
 
314
314
  Supported compatibility imports: `cursor`, `claude-code`, `claude-desktop`, `vscode`, `windsurf`, `codex`
315
315
 
316
- `pi-mcp-adapter init` detects these host-specific configs and adds missing imports to `~/.pi/agent/mcp.json` for you.
316
+ `pi-mcp-adapter init` detects these host-specific configs and adds missing imports to the Pi agent dir config for you.
317
317
 
318
318
  ### Project Config
319
319
 
package/agent-dir.ts ADDED
@@ -0,0 +1,20 @@
1
+ import { homedir } from "node:os";
2
+ import { join, resolve } from "node:path";
3
+
4
+ export function getAgentDir(): string {
5
+ const configured = process.env.PI_CODING_AGENT_DIR?.trim();
6
+ if (!configured) {
7
+ return join(homedir(), ".pi", "agent");
8
+ }
9
+ if (configured === "~") {
10
+ return homedir();
11
+ }
12
+ if (configured.startsWith("~/")) {
13
+ return resolve(homedir(), configured.slice(2));
14
+ }
15
+ return resolve(configured);
16
+ }
17
+
18
+ export function getAgentPath(...segments: string[]): string {
19
+ return join(getAgentDir(), ...segments);
20
+ }
package/cli.js CHANGED
@@ -6,7 +6,17 @@ import os from "node:os";
6
6
  import { pathToFileURL } from "node:url";
7
7
 
8
8
  const HOME = os.homedir();
9
- const PI_CONFIG_PATH = path.join(HOME, ".pi", "agent", "mcp.json");
9
+
10
+ function expandHome(input) {
11
+ if (input === "~") return HOME;
12
+ if (input.startsWith("~/")) return path.resolve(HOME, input.slice(2));
13
+ return path.resolve(input);
14
+ }
15
+
16
+ const AGENT_DIR = process.env.PI_CODING_AGENT_DIR?.trim()
17
+ ? expandHome(process.env.PI_CODING_AGENT_DIR.trim())
18
+ : path.join(HOME, ".pi", "agent");
19
+ const PI_CONFIG_PATH = path.join(AGENT_DIR, "mcp.json");
10
20
  const GENERIC_GLOBAL_CONFIG_PATH = path.join(HOME, ".config", "mcp", "mcp.json");
11
21
  const PROJECT_CONFIG_PATH = path.resolve(process.cwd(), ".mcp.json");
12
22
  const PROJECT_PI_CONFIG_PATH = path.resolve(process.cwd(), ".pi", "mcp.json");
package/config.ts CHANGED
@@ -2,9 +2,9 @@
2
2
  import { existsSync, readFileSync, writeFileSync, mkdirSync, renameSync } from "node:fs";
3
3
  import { homedir } from "node:os";
4
4
  import { dirname, join, resolve } from "node:path";
5
+ import { getAgentPath } from "./agent-dir.js";
5
6
  import type { McpConfig, ServerEntry, McpSettings, ImportKind, ServerProvenance } from "./types.js";
6
7
 
7
- const PI_GLOBAL_CONFIG_PATH = join(homedir(), ".pi", "agent", "mcp.json");
8
8
  const GENERIC_GLOBAL_CONFIG_PATH = join(homedir(), ".config", "mcp", "mcp.json");
9
9
  const PROJECT_CONFIG_NAME = ".mcp.json";
10
10
  const PROJECT_PI_CONFIG_NAME = ".pi/mcp.json";
@@ -90,7 +90,7 @@ export interface ConfigWritePreview {
90
90
  }
91
91
 
92
92
  export function getPiGlobalConfigPath(overridePath?: string): string {
93
- return overridePath ? resolve(overridePath) : PI_GLOBAL_CONFIG_PATH;
93
+ return overridePath ? resolve(overridePath) : getAgentPath("mcp.json");
94
94
  }
95
95
 
96
96
  export function getGenericGlobalConfigPath(): string {
package/mcp-auth.ts CHANGED
@@ -5,12 +5,13 @@
5
5
  * and PKCE state for MCP servers. Maintains backward compatibility with
6
6
  * per-server directory structure.
7
7
  *
8
- * Token storage location: ~/.pi/agent/mcp-oauth/<server>/tokens.json
8
+ * Token storage location: $MCP_OAUTH_DIR/<server>/tokens.json when set,
9
+ * otherwise <Pi agent dir>/mcp-oauth/<server>/tokens.json
9
10
  */
10
11
 
11
12
  import { mkdirSync, readFileSync, writeFileSync, existsSync, rmSync } from 'fs';
12
13
  import { join } from 'path';
13
- import { homedir } from 'os';
14
+ import { getAgentPath } from './agent-dir.js';
14
15
 
15
16
  /** OAuth token storage format */
16
17
  export interface StoredTokens {
@@ -39,9 +40,8 @@ export interface AuthEntry {
39
40
 
40
41
  // Base directory for auth storage - can be overridden via env var for testing
41
42
  function getAuthBaseDir(): string {
42
- return process.env.MCP_OAUTH_DIR
43
- ? process.env.MCP_OAUTH_DIR
44
- : join(homedir(), '.pi', 'agent', 'mcp-oauth');
43
+ const override = process.env.MCP_OAUTH_DIR?.trim();
44
+ return override ? override : getAgentPath('mcp-oauth');
45
45
  }
46
46
 
47
47
  /**
@@ -178,7 +178,7 @@ export async function ensureCallbackServer(options: EnsureCallbackServerOptions
178
178
  reject(err)
179
179
  })
180
180
 
181
- candidateServer.listen(candidatePort, "127.0.0.1", () => {
181
+ candidateServer.listen(candidatePort, "localhost", () => {
182
182
  resolve()
183
183
  })
184
184
  })
@@ -29,7 +29,7 @@ import {
29
29
 
30
30
  // Callback server configuration
31
31
  const DEFAULT_OAUTH_CALLBACK_PORT = 19876
32
- const OAUTH_CALLBACK_PATH = "/mcp/oauth/callback"
32
+ const OAUTH_CALLBACK_PATH = "/callback"
33
33
 
34
34
  let configuredOAuthCallbackPort = DEFAULT_OAUTH_CALLBACK_PORT
35
35
 
@@ -89,7 +89,7 @@ export class McpOAuthProvider implements OAuthClientProvider {
89
89
  */
90
90
  get redirectUrl(): string | undefined {
91
91
  if (this.usesClientCredentials) return undefined
92
- return `http://127.0.0.1:${getOAuthCallbackPort()}${OAUTH_CALLBACK_PATH}`
92
+ return `http://localhost:${getOAuthCallbackPort()}${OAUTH_CALLBACK_PATH}`
93
93
  }
94
94
 
95
95
  /**
@@ -459,7 +459,7 @@ export class McpSetupPanel {
459
459
  this.callbacks.previewImports(this.discovery.imports.filter((entry) => this.selectedImports.has(entry.kind)).map((entry) => entry.kind)),
460
460
  [
461
461
  `Detected imports: ${this.discovery.imports.map((entry) => `${entry.kind} (${entry.serverCount} servers)`).join(", ")}`,
462
- "Selected imports are written into `~/.pi/agent/mcp.json` as Pi-owned compatibility state.",
462
+ "Selected imports are written into the Pi agent dir config as Pi-owned compatibility state.",
463
463
  ],
464
464
  );
465
465
  case "view-example":
@@ -480,7 +480,7 @@ export class McpSetupPanel {
480
480
  return this.formatPreview([
481
481
  "Read order:",
482
482
  "1. ~/.config/mcp/mcp.json",
483
- "2. ~/.pi/agent/mcp.json",
483
+ "2. <Pi agent dir>/mcp.json",
484
484
  "3. .mcp.json",
485
485
  "4. .pi/mcp.json",
486
486
  "Pi writes compatibility imports and adapter-only overrides to Pi-owned files.",
package/metadata-cache.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  // metadata-cache.ts - Persistent MCP metadata cache
2
2
  import { existsSync, readFileSync, writeFileSync, renameSync, mkdirSync } from "node:fs";
3
- import { homedir } from "node:os";
4
- import { dirname, join } from "node:path";
3
+ import { dirname } from "node:path";
4
+ import { getAgentPath } from "./agent-dir.js";
5
5
  import { createHash } from "node:crypto";
6
6
  import { getToolUiResourceUri } from "@modelcontextprotocol/ext-apps/app-bridge";
7
7
  import type { McpTool, McpResource, ServerEntry, ToolMetadata } from "./types.js";
@@ -11,7 +11,6 @@ import { extractToolUiStreamMode } from "./utils.js";
11
11
 
12
12
  const CACHE_VERSION = 1;
13
13
  const CACHE_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
14
- const CACHE_PATH = join(homedir(), ".pi", "agent", "mcp-cache.json");
15
14
 
16
15
  export interface CachedTool {
17
16
  name: string;
@@ -40,13 +39,14 @@ export interface MetadataCache {
40
39
  }
41
40
 
42
41
  export function getMetadataCachePath(): string {
43
- return CACHE_PATH;
42
+ return getAgentPath("mcp-cache.json");
44
43
  }
45
44
 
46
45
  export function loadMetadataCache(): MetadataCache | null {
47
- if (!existsSync(CACHE_PATH)) return null;
46
+ const cachePath = getMetadataCachePath();
47
+ if (!existsSync(cachePath)) return null;
48
48
  try {
49
- const raw = JSON.parse(readFileSync(CACHE_PATH, "utf-8"));
49
+ const raw = JSON.parse(readFileSync(cachePath, "utf-8"));
50
50
  if (!raw || typeof raw !== "object") return null;
51
51
  if (raw.version !== CACHE_VERSION) return null;
52
52
  if (!raw.servers || typeof raw.servers !== "object") return null;
@@ -57,13 +57,14 @@ export function loadMetadataCache(): MetadataCache | null {
57
57
  }
58
58
 
59
59
  export function saveMetadataCache(cache: MetadataCache): void {
60
- const dir = dirname(CACHE_PATH);
60
+ const cachePath = getMetadataCachePath();
61
+ const dir = dirname(cachePath);
61
62
  mkdirSync(dir, { recursive: true });
62
63
 
63
64
  let merged: MetadataCache = { version: CACHE_VERSION, servers: {} };
64
65
  try {
65
- if (existsSync(CACHE_PATH)) {
66
- const existing = JSON.parse(readFileSync(CACHE_PATH, "utf-8")) as MetadataCache;
66
+ if (existsSync(cachePath)) {
67
+ const existing = JSON.parse(readFileSync(cachePath, "utf-8")) as MetadataCache;
67
68
  if (existing && existing.version === CACHE_VERSION && existing.servers) {
68
69
  merged.servers = { ...existing.servers };
69
70
  }
@@ -75,9 +76,9 @@ export function saveMetadataCache(cache: MetadataCache): void {
75
76
  merged.version = CACHE_VERSION;
76
77
  merged.servers = { ...merged.servers, ...cache.servers };
77
78
 
78
- const tmpPath = `${CACHE_PATH}.${process.pid}.tmp`;
79
+ const tmpPath = `${cachePath}.${process.pid}.tmp`;
79
80
  writeFileSync(tmpPath, JSON.stringify(merged, null, 2), "utf-8");
80
- renameSync(tmpPath, CACHE_PATH);
81
+ renameSync(tmpPath, cachePath);
81
82
  }
82
83
 
83
84
  export function computeServerHash(definition: ServerEntry): string {
package/npx-resolver.ts CHANGED
@@ -1,12 +1,11 @@
1
1
  // npx-resolver.ts - Resolve npx/npm exec binaries to avoid npm parent processes
2
2
  import { existsSync, readFileSync, realpathSync, readdirSync, statSync, writeFileSync, renameSync, mkdirSync, openSync, readSync, closeSync } from "node:fs";
3
- import { homedir } from "node:os";
4
3
  import { join, dirname, extname, resolve, sep } from "node:path";
4
+ import { getAgentPath } from "./agent-dir.js";
5
5
  import { spawn, spawnSync } from "node:child_process";
6
6
 
7
7
  const CACHE_VERSION = 1;
8
8
  const CACHE_TTL_MS = 24 * 60 * 60 * 1000;
9
- const CACHE_PATH = join(homedir(), ".pi", "agent", "mcp-npx-cache.json");
10
9
 
11
10
  interface NpxCacheEntry {
12
11
  resolvedBin: string;
@@ -367,10 +366,15 @@ function getNpmCacheDir(): string | null {
367
366
  return null;
368
367
  }
369
368
 
369
+ function getNpxCachePath(): string {
370
+ return getAgentPath("mcp-npx-cache.json");
371
+ }
372
+
370
373
  function loadCache(): NpxCache | null {
371
- if (!existsSync(CACHE_PATH)) return null;
374
+ const cachePath = getNpxCachePath();
375
+ if (!existsSync(cachePath)) return null;
372
376
  try {
373
- const raw = JSON.parse(readFileSync(CACHE_PATH, "utf-8"));
377
+ const raw = JSON.parse(readFileSync(cachePath, "utf-8"));
374
378
  if (!raw || typeof raw !== "object") return null;
375
379
  if (raw.version !== CACHE_VERSION) return null;
376
380
  if (!raw.entries || typeof raw.entries !== "object") return null;
@@ -381,13 +385,14 @@ function loadCache(): NpxCache | null {
381
385
  }
382
386
 
383
387
  function saveCacheEntry(key: string, entry: NpxCacheEntry): void {
384
- const dir = dirname(CACHE_PATH);
388
+ const cachePath = getNpxCachePath();
389
+ const dir = dirname(cachePath);
385
390
  mkdirSync(dir, { recursive: true });
386
391
 
387
392
  let merged: NpxCache = { version: CACHE_VERSION, entries: {} };
388
393
  try {
389
- if (existsSync(CACHE_PATH)) {
390
- const existing = JSON.parse(readFileSync(CACHE_PATH, "utf-8")) as NpxCache;
394
+ if (existsSync(cachePath)) {
395
+ const existing = JSON.parse(readFileSync(cachePath, "utf-8")) as NpxCache;
391
396
  if (existing && existing.version === CACHE_VERSION && existing.entries) {
392
397
  merged.entries = { ...existing.entries };
393
398
  }
@@ -397,9 +402,9 @@ function saveCacheEntry(key: string, entry: NpxCacheEntry): void {
397
402
  }
398
403
 
399
404
  merged.entries[key] = entry;
400
- const tmpPath = `${CACHE_PATH}.${process.pid}.tmp`;
405
+ const tmpPath = `${cachePath}.${process.pid}.tmp`;
401
406
  writeFileSync(tmpPath, JSON.stringify(merged, null, 2), "utf-8");
402
- renameSync(tmpPath, CACHE_PATH);
407
+ renameSync(tmpPath, cachePath);
403
408
  }
404
409
 
405
410
  function safeRealpath(path: string): string {
package/oauth-handler.ts CHANGED
@@ -1,19 +1,22 @@
1
1
  // oauth-handler.ts - OAuth token management for MCP servers
2
2
  import { existsSync, readFileSync } from "node:fs";
3
3
  import { join } from "node:path";
4
- import { homedir } from "node:os";
4
+ import { getAgentPath } from "./agent-dir.js";
5
5
  import type { OAuthTokens } from "@modelcontextprotocol/sdk/shared/auth.js";
6
6
 
7
7
  // Token storage path for a server
8
8
  function getTokensPath(serverName: string): string {
9
- return join(homedir(), ".pi", "agent", "mcp-oauth", serverName, "tokens.json");
9
+ const override = process.env.MCP_OAUTH_DIR?.trim();
10
+ const authDir = override ? override : getAgentPath("mcp-oauth");
11
+ return join(authDir, serverName, "tokens.json");
10
12
  }
11
13
 
12
14
  /**
13
15
  * Get stored OAuth tokens for a server (if any).
14
16
  * Returns undefined if no tokens or tokens are expired.
15
17
  *
16
- * Token file location: ~/.pi/agent/mcp-oauth/<server>/tokens.json
18
+ * Token file location: $MCP_OAUTH_DIR/<server>/tokens.json when set,
19
+ * otherwise <Pi agent dir>/mcp-oauth/<server>/tokens.json
17
20
  *
18
21
  * Expected format:
19
22
  * {
@@ -1,6 +1,6 @@
1
1
  import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync } from "node:fs";
2
- import { homedir } from "node:os";
3
- import { dirname, join } from "node:path";
2
+ import { dirname } from "node:path";
3
+ import { getAgentPath } from "./agent-dir.js";
4
4
 
5
5
  export interface McpOnboardingState {
6
6
  version: 1;
@@ -16,7 +16,7 @@ const DEFAULT_STATE: McpOnboardingState = {
16
16
  };
17
17
 
18
18
  export function getOnboardingStatePath(): string {
19
- return join(homedir(), ".pi", "agent", "mcp-onboarding.json");
19
+ return getAgentPath("mcp-onboarding.json");
20
20
  }
21
21
 
22
22
  export function loadOnboardingState(): McpOnboardingState {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-mcp-adapter",
3
- "version": "2.5.0",
3
+ "version": "2.5.2",
4
4
  "description": "MCP (Model Context Protocol) adapter extension for Pi coding agent",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -37,6 +37,7 @@
37
37
  },
38
38
  "files": [
39
39
  "cli.js",
40
+ "agent-dir.ts",
40
41
  "index.ts",
41
42
  "state.ts",
42
43
  "utils.ts",