pi-mcp-extension 1.7.0 → 1.7.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-mcp-extension",
3
- "version": "1.7.0",
3
+ "version": "1.7.1",
4
4
  "description": "MCP (Model Context Protocol) client extension for the Pi coding agent — connect Pi to any MCP server",
5
5
  "type": "module",
6
6
  "keywords": [
package/src/config.ts CHANGED
@@ -2,8 +2,9 @@
2
2
  * Configuration loading, validation, and merging for pi-mcp.
3
3
  *
4
4
  * Config file locations (Pi-native convention, highest priority first):
5
- * 1. <cwd>/.pi/mcp.json — project-level config
6
- * 2. ~/.pi/agent/mcp.json — global config
5
+ * 1. <cwd>/.pi/mcp.json — project-level config
6
+ * 2. <agent dir>/mcp.json — global config; the agent dir is what Pi itself uses:
7
+ * $PI_CODING_AGENT_DIR when set (Pi profiles / alternative agent dirs), else ~/.pi/agent
7
8
  *
8
9
  * Project servers/settings override global servers/settings per-key (shallow merge).
9
10
  * No deep merge, no env var interpolation — WYSIWYG config.
@@ -118,6 +119,25 @@ const McpConfigSchema = z.object({
118
119
  mcpServers: z.record(ServerConfigSchema).default({}),
119
120
  });
120
121
 
122
+ /**
123
+ * Project-level file: `settings` keys are optional and carry NO defaults, so a project file
124
+ * only overrides what it actually states. (Parsing it with SettingsSchema filled in the
125
+ * defaults — e.g. toolPrefix "mcp" — and the merge then overwrote the global settings.)
126
+ */
127
+ const ProjectSettingsSchema = z.object({
128
+ toolPrefix: z
129
+ .string()
130
+ .regex(/^[a-zA-Z0-9_]+$/, "toolPrefix must match [a-zA-Z0-9_]")
131
+ .optional(),
132
+ requestTimeoutMs: z.number().positive().optional(),
133
+ maxRetries: z.number().int().min(0).max(10).optional(),
134
+ });
135
+ const ProjectConfigSchema = z.object({
136
+ settings: ProjectSettingsSchema.default({}),
137
+ mcpServers: z.record(ServerConfigSchema).default({}),
138
+ });
139
+ type ProjectConfig = z.output<typeof ProjectConfigSchema>;
140
+
121
141
  // ─── Public Types ─────────────────────────────────────────────────────────────
122
142
 
123
143
  export type AuthConfig = z.output<typeof AuthConfigSchema>;
@@ -139,7 +159,15 @@ async function readJsonFile(path: string): Promise<unknown | null> {
139
159
  }
140
160
 
141
161
  function parseConfig(raw: unknown, sourcePath: string): McpConfig {
142
- const result = McpConfigSchema.safeParse(raw);
162
+ return parseWith(McpConfigSchema, raw, sourcePath);
163
+ }
164
+
165
+ function parseWith<S extends z.ZodTypeAny>(
166
+ schema: S,
167
+ raw: unknown,
168
+ sourcePath: string,
169
+ ): z.output<S> {
170
+ const result = schema.safeParse(raw);
143
171
  if (!result.success) {
144
172
  const issues = result.error.issues
145
173
  .map((i) => ` ${i.path.join(".")}: ${i.message}`)
@@ -155,22 +183,41 @@ function parseConfig(raw: unknown, sourcePath: string): McpConfig {
155
183
 
156
184
  function mergeConfigs(
157
185
  globalCfg: McpConfig,
158
- projectCfg: McpConfig,
186
+ projectCfg: ProjectConfig,
159
187
  ): McpConfig {
188
+ // Only the settings the project file states may override the global ones.
189
+ const stated = Object.fromEntries(
190
+ Object.entries(projectCfg.settings).filter(([, v]) => v !== undefined),
191
+ ) as Partial<Settings>;
160
192
  return {
161
193
  // Shallow spread: project settings override global settings per key
162
- settings: { ...globalCfg.settings, ...projectCfg.settings },
194
+ settings: { ...globalCfg.settings, ...stated },
163
195
  // Per-server override: project server entry completely replaces global entry with same name
164
196
  mcpServers: { ...globalCfg.mcpServers, ...projectCfg.mcpServers },
165
197
  };
166
198
  }
167
199
 
168
200
  /**
169
- * Load and merge global (~/.pi/agent/mcp.json) and project (<cwd>/.pi/mcp.json) configs.
201
+ * Pi's agent directory, resolved the way Pi resolves it (`getAgentDir()` in
202
+ * @mariozechner/pi-coding-agent): $PI_CODING_AGENT_DIR when set (tilde expanded), else
203
+ * ~/.pi/agent. Pi profiles and alternative agent dirs only work through that variable.
204
+ */
205
+ export function agentDir(env: NodeJS.ProcessEnv = process.env): string {
206
+ const fromEnv = env.PI_CODING_AGENT_DIR;
207
+ if (fromEnv && fromEnv.length > 0) {
208
+ if (fromEnv === "~") return homedir();
209
+ if (fromEnv.startsWith("~/")) return join(homedir(), fromEnv.slice(2));
210
+ return fromEnv;
211
+ }
212
+ return join(homedir(), ".pi", "agent");
213
+ }
214
+
215
+ /**
216
+ * Load and merge global (<agent dir>/mcp.json) and project (<cwd>/.pi/mcp.json) configs.
170
217
  * Project config takes precedence over global config.
171
218
  * Returns a fully validated, merged config.
172
219
  */
173
- export async function loadConfig(cwd: string, globalPath = join(homedir(), ".pi", "agent", "mcp.json")): Promise<McpConfig> {
220
+ export async function loadConfig(cwd: string, globalPath = join(agentDir(), "mcp.json")): Promise<McpConfig> {
174
221
  const projectPath = join(cwd, ".pi", "mcp.json");
175
222
 
176
223
  const [globalRaw, projectRaw] = await Promise.all([
@@ -189,6 +236,6 @@ export async function loadConfig(cwd: string, globalPath = join(homedir(), ".pi"
189
236
 
190
237
  if (projectRaw === null) return globalCfg;
191
238
 
192
- const projectCfg = parseConfig(projectRaw, projectPath);
239
+ const projectCfg = parseWith(ProjectConfigSchema, projectRaw, projectPath);
193
240
  return mergeConfigs(globalCfg, projectCfg);
194
241
  }
@@ -15,8 +15,8 @@ import type { OAuthClientProvider, OAuthDiscoveryState } from "@modelcontextprot
15
15
  import type { OAuthClientMetadata, OAuthClientInformationMixed, OAuthTokens } from "@modelcontextprotocol/sdk/shared/auth.js";
16
16
  import { readFile, writeFile, mkdir, unlink, chmod, open, stat } from "node:fs/promises";
17
17
  import { join } from "node:path";
18
- import { homedir } from "node:os";
19
18
  import { createHash } from "node:crypto";
19
+ import { agentDir } from "./config.js";
20
20
 
21
21
  // ─── Types ────────────────────────────────────────────────────────────────────
22
22
 
@@ -84,7 +84,7 @@ interface StoredState {
84
84
  // ─── File helpers ─────────────────────────────────────────────────────────────
85
85
 
86
86
  function authDir(storageDir?: string): string {
87
- return storageDir ?? join(homedir(), ".pi", "agent", "mcp-auth");
87
+ return storageDir ?? join(agentDir(), "mcp-auth");
88
88
  }
89
89
 
90
90
  function statePath(serverName: string, storageDir?: string): string {