pi-mcp-adapter 2.5.1 → 2.5.3
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 +10 -0
- package/README.md +9 -8
- package/agent-dir.ts +20 -0
- package/cli.js +11 -1
- package/config.ts +2 -2
- package/mcp-auth.ts +5 -5
- package/mcp-setup-panel.ts +2 -2
- package/metadata-cache.ts +17 -16
- package/npx-resolver.ts +14 -9
- package/oauth-handler.ts +6 -3
- package/onboarding-state.ts +3 -3
- package/package.json +2 -1
- package/server-manager.ts +6 -19
- package/utils.ts +29 -1
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.3] - 2026-05-01
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Added environment variable and `~` expansion for stdio server `cwd` values.
|
|
14
|
+
|
|
15
|
+
## [2.5.2] - 2026-04-29
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
- 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.
|
|
19
|
+
|
|
10
20
|
## [2.5.1] - 2026-04-24
|
|
11
21
|
|
|
12
22
|
### Fixed
|
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
|
-
-
|
|
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.
|
|
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
|
-
|
|
|
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`.
|
|
@@ -120,9 +120,10 @@ Pi-specific files are the write targets for imported or shared global servers wh
|
|
|
120
120
|
|-------|-------------|
|
|
121
121
|
| `command` | Executable for stdio transport |
|
|
122
122
|
| `args` | Command arguments |
|
|
123
|
-
| `env` | Environment variables
|
|
124
|
-
| `cwd` | Working directory |
|
|
123
|
+
| `env` | Environment variables; supports `${VAR}` and `$env:VAR` interpolation |
|
|
124
|
+
| `cwd` | Working directory; supports `${VAR}`, `$env:VAR`, and `~` expansion |
|
|
125
125
|
| `url` | HTTP endpoint (StreamableHTTP with SSE fallback) |
|
|
126
|
+
| `headers` | HTTP headers; supports `${VAR}` and `$env:VAR` interpolation |
|
|
126
127
|
| `auth` | `"bearer"` or `"oauth"` |
|
|
127
128
|
| `oauth.grantType` | `"authorization_code"` (default) or `"client_credentials"` for non-interactive machine auth |
|
|
128
129
|
| `bearerToken` / `bearerTokenEnv` | Token or env var name |
|
|
@@ -231,7 +232,7 @@ To exclude specific tools while still using `directTools: true`, add `excludeToo
|
|
|
231
232
|
|
|
232
233
|
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
234
|
|
|
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>`.
|
|
235
|
+
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
236
|
|
|
236
237
|
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
238
|
|
|
@@ -313,7 +314,7 @@ Shared MCP files are loaded automatically. Use `imports` only for host-specific
|
|
|
313
314
|
|
|
314
315
|
Supported compatibility imports: `cursor`, `claude-code`, `claude-desktop`, `vscode`, `windsurf`, `codex`
|
|
315
316
|
|
|
316
|
-
`pi-mcp-adapter init` detects these host-specific configs and adds missing imports to
|
|
317
|
+
`pi-mcp-adapter init` detects these host-specific configs and adds missing imports to the Pi agent dir config for you.
|
|
317
318
|
|
|
318
319
|
### Project Config
|
|
319
320
|
|
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
|
-
|
|
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) :
|
|
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:
|
|
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 {
|
|
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
|
-
|
|
43
|
-
|
|
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
|
/**
|
package/mcp-setup-panel.ts
CHANGED
|
@@ -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
|
|
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.
|
|
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,17 +1,16 @@
|
|
|
1
1
|
// metadata-cache.ts - Persistent MCP metadata cache
|
|
2
2
|
import { existsSync, readFileSync, writeFileSync, renameSync, mkdirSync } from "node:fs";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
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";
|
|
8
8
|
import { formatToolName, isToolExcluded } from "./types.js";
|
|
9
9
|
import { resourceNameToToolName } from "./resource-tools.js";
|
|
10
|
-
import { extractToolUiStreamMode } from "./utils.js";
|
|
10
|
+
import { extractToolUiStreamMode, interpolateEnvRecord, resolveConfigPath } 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
|
|
42
|
+
return getAgentPath("mcp-cache.json");
|
|
44
43
|
}
|
|
45
44
|
|
|
46
45
|
export function loadMetadataCache(): MetadataCache | null {
|
|
47
|
-
|
|
46
|
+
const cachePath = getMetadataCachePath();
|
|
47
|
+
if (!existsSync(cachePath)) return null;
|
|
48
48
|
try {
|
|
49
|
-
const raw = JSON.parse(readFileSync(
|
|
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
|
|
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(
|
|
66
|
-
const existing = JSON.parse(readFileSync(
|
|
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 = `${
|
|
79
|
+
const tmpPath = `${cachePath}.${process.pid}.tmp`;
|
|
79
80
|
writeFileSync(tmpPath, JSON.stringify(merged, null, 2), "utf-8");
|
|
80
|
-
renameSync(tmpPath,
|
|
81
|
+
renameSync(tmpPath, cachePath);
|
|
81
82
|
}
|
|
82
83
|
|
|
83
84
|
export function computeServerHash(definition: ServerEntry): string {
|
|
@@ -87,12 +88,12 @@ export function computeServerHash(definition: ServerEntry): string {
|
|
|
87
88
|
const identity: Record<string, unknown> = {
|
|
88
89
|
command: definition.command,
|
|
89
90
|
args: definition.args,
|
|
90
|
-
env: definition.env,
|
|
91
|
-
cwd: definition.cwd,
|
|
91
|
+
env: interpolateEnvRecord(definition.env),
|
|
92
|
+
cwd: resolveConfigPath(definition.cwd),
|
|
92
93
|
url: definition.url,
|
|
93
|
-
headers: definition.headers,
|
|
94
|
+
headers: interpolateEnvRecord(definition.headers),
|
|
94
95
|
auth: definition.auth,
|
|
95
|
-
bearerToken: definition.bearerToken,
|
|
96
|
+
bearerToken: definition.bearerToken ?? (definition.bearerTokenEnv ? process.env[definition.bearerTokenEnv] : undefined),
|
|
96
97
|
bearerTokenEnv: definition.bearerTokenEnv,
|
|
97
98
|
exposeResources: definition.exposeResources,
|
|
98
99
|
excludeTools: definition.excludeTools,
|
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
|
-
|
|
374
|
+
const cachePath = getNpxCachePath();
|
|
375
|
+
if (!existsSync(cachePath)) return null;
|
|
372
376
|
try {
|
|
373
|
-
const raw = JSON.parse(readFileSync(
|
|
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
|
|
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(
|
|
390
|
-
const existing = JSON.parse(readFileSync(
|
|
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 = `${
|
|
405
|
+
const tmpPath = `${cachePath}.${process.pid}.tmp`;
|
|
401
406
|
writeFileSync(tmpPath, JSON.stringify(merged, null, 2), "utf-8");
|
|
402
|
-
renameSync(tmpPath,
|
|
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 {
|
|
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
|
-
|
|
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:
|
|
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
|
* {
|
package/onboarding-state.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync } from "node:fs";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
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
|
|
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.
|
|
3
|
+
"version": "2.5.3",
|
|
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",
|
package/server-manager.ts
CHANGED
|
@@ -17,6 +17,7 @@ import { logger } from "./logger.js";
|
|
|
17
17
|
import { McpOAuthProvider } from "./mcp-oauth-provider.js";
|
|
18
18
|
import { supportsOAuth } from "./mcp-auth-flow.js";
|
|
19
19
|
import { registerSamplingHandler, type ServerSamplingConfig } from "./sampling-handler.js";
|
|
20
|
+
import { interpolateEnvRecord, resolveConfigPath } from "./utils.js";
|
|
20
21
|
|
|
21
22
|
interface ServerConnection {
|
|
22
23
|
client: Client;
|
|
@@ -91,7 +92,7 @@ export class McpServerManager {
|
|
|
91
92
|
command,
|
|
92
93
|
args,
|
|
93
94
|
env: resolveEnv(definition.env),
|
|
94
|
-
cwd: definition.cwd,
|
|
95
|
+
cwd: resolveConfigPath(definition.cwd),
|
|
95
96
|
stderr: definition.debug ? "inherit" : "ignore",
|
|
96
97
|
});
|
|
97
98
|
} else if (definition.url) {
|
|
@@ -362,28 +363,14 @@ function resolveEnv(env?: Record<string, string>): Record<string, string> {
|
|
|
362
363
|
}
|
|
363
364
|
|
|
364
365
|
if (!env) return resolved;
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
resolved[key] = value
|
|
369
|
-
.replace(/\$\{(\w+)\}/g, (_, name) => process.env[name] ?? "")
|
|
370
|
-
.replace(/\$env:(\w+)/g, (_, name) => process.env[name] ?? "");
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
return resolved;
|
|
366
|
+
|
|
367
|
+
const overrides = interpolateEnvRecord(env);
|
|
368
|
+
return overrides ? { ...resolved, ...overrides } : resolved;
|
|
374
369
|
}
|
|
375
370
|
|
|
376
371
|
/**
|
|
377
372
|
* Resolve headers with environment variable interpolation.
|
|
378
373
|
*/
|
|
379
374
|
function resolveHeaders(headers?: Record<string, string>): Record<string, string> | undefined {
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
const resolved: Record<string, string> = {};
|
|
383
|
-
for (const [key, value] of Object.entries(headers)) {
|
|
384
|
-
resolved[key] = value
|
|
385
|
-
.replace(/\$\{(\w+)\}/g, (_, name) => process.env[name] ?? "")
|
|
386
|
-
.replace(/\$env:(\w+)/g, (_, name) => process.env[name] ?? "");
|
|
387
|
-
}
|
|
388
|
-
return resolved;
|
|
375
|
+
return interpolateEnvRecord(headers);
|
|
389
376
|
}
|
package/utils.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
2
|
-
import { platform } from "node:os";
|
|
2
|
+
import { homedir, platform } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
3
4
|
import type { McpConfig } from "./types.js";
|
|
4
5
|
|
|
5
6
|
async function execOpen(pi: ExtensionAPI, target: string, browser?: string) {
|
|
@@ -58,6 +59,33 @@ export function getConfigPathFromArgv(): string | undefined {
|
|
|
58
59
|
return undefined;
|
|
59
60
|
}
|
|
60
61
|
|
|
62
|
+
export function interpolateEnvVars(value: string): string {
|
|
63
|
+
return value
|
|
64
|
+
.replace(/\$\{(\w+)\}/g, (_, name) => process.env[name] ?? "")
|
|
65
|
+
.replace(/\$env:(\w+)/g, (_, name) => process.env[name] ?? "");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function interpolateEnvRecord(values: Record<string, string> | undefined): Record<string, string> | undefined {
|
|
69
|
+
if (!values) return undefined;
|
|
70
|
+
|
|
71
|
+
const resolved: Record<string, string> = {};
|
|
72
|
+
for (const [key, value] of Object.entries(values)) {
|
|
73
|
+
resolved[key] = interpolateEnvVars(value);
|
|
74
|
+
}
|
|
75
|
+
return resolved;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function resolveConfigPath(value: string | undefined): string | undefined {
|
|
79
|
+
if (value === undefined) return undefined;
|
|
80
|
+
|
|
81
|
+
const resolved = interpolateEnvVars(value);
|
|
82
|
+
if (resolved === "~") return homedir();
|
|
83
|
+
if (resolved.startsWith("~/") || resolved.startsWith("~\\")) {
|
|
84
|
+
return join(homedir(), resolved.slice(2));
|
|
85
|
+
}
|
|
86
|
+
return resolved;
|
|
87
|
+
}
|
|
88
|
+
|
|
61
89
|
export function truncateAtWord(text: string, target: number): string {
|
|
62
90
|
if (!text || text.length <= target) return text;
|
|
63
91
|
|