pi-mcp-client 0.8.0 → 0.8.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/README.md CHANGED
@@ -41,7 +41,9 @@ permissions. Tool activation isn't a per-call approval prompt. See
41
41
  ## ⚙️ Configuration
42
42
 
43
43
  Store server definitions in `~/.pi/agent/mcp.json` or a trusted project's
44
- `.mcp.json`. Project definitions replace same-named global definitions in full.
44
+ `.mcp.json`. Project servers load only after an explicit
45
+ [trust decision](docs/behavior.md#project-trust). Project definitions replace
46
+ same-named global definitions in full.
45
47
  You can edit these files or use `/mcp add` and `/mcp remove`. To reuse a
46
48
  Claude/Cursor JSON or Codex TOML file, run `/mcp import --scope global <path>` and
47
49
  review the selected connections before saving.
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  // src/index.ts
2
+ import { existsSync } from "node:fs";
2
3
  import { join as join4 } from "node:path";
3
4
  import {
4
5
  BorderedLoader as BorderedLoader2,
@@ -2329,7 +2330,7 @@ async function inspectConfigScopes(agentDir, cwd, trusted) {
2329
2330
  async function updateServerConfig(agentDir, cwd, trusted, mutation, validate) {
2330
2331
  const scoped = mutation.action !== "toggle";
2331
2332
  if (scoped && mutation.scope === "project" && !trusted)
2332
- throw new ConfigMutationError("Project configuration requires a trusted project. Use global scope or trust the project first.");
2333
+ throw new ConfigMutationError("Project configuration requires a trusted project. Use global scope, or run /trust to trust this folder first.");
2333
2334
  const paths = [join(agentDir, "mcp.json"), ...trusted ? [join(cwd, ".mcp.json")] : []];
2334
2335
  const targets = await Promise.all(paths.map(configTarget));
2335
2336
  if (scoped && targets.length === 2 && targets[0] === targets[1])
@@ -3973,16 +3974,16 @@ function importAction(name, scope, scopes) {
3973
3974
  if (scope === "project" && Object.hasOwn(scopes.global, name)) return "Override global definition";
3974
3975
  return `Add ${scope} definition`;
3975
3976
  }
3976
- async function runImportCommand(input, ctx, agentDir, signal, assertCurrent, save) {
3977
+ async function runImportCommand(input, ctx, agentDir, projectTrusted, signal, assertCurrent, save) {
3977
3978
  if (!ctx.hasUI) throw new ConfigMutationError("Configuration imports require an interactive session (TUI or RPC).");
3978
3979
  const { scope, path } = parseImportCommand(input);
3979
- const trusted = ctx.isProjectTrusted();
3980
+ const trusted = projectTrusted();
3980
3981
  if (scope === "project" && !trusted)
3981
- throw new ConfigMutationError("Project configuration requires a trusted project. Use global scope or trust the project first.");
3982
+ throw new ConfigMutationError("Project configuration requires a trusted project. Use global scope, or run /trust to trust this folder first.");
3982
3983
  const guard = () => {
3983
3984
  signal.throwIfAborted();
3984
3985
  assertCurrent();
3985
- if (ctx.isProjectTrusted() !== trusted)
3986
+ if (projectTrusted() !== trusted)
3986
3987
  throw new ConfigMutationError("Project trust changed during the preview. Run /mcp import again; nothing was saved.");
3987
3988
  };
3988
3989
  const select = async (title, choices) => {
@@ -5958,6 +5959,41 @@ function statusPanel(snapshot, theme) {
5958
5959
  };
5959
5960
  }
5960
5961
 
5962
+ // src/trust.ts
5963
+ import {
5964
+ hasTrustRequiringProjectResources,
5965
+ ProjectTrustStore,
5966
+ SettingsManager
5967
+ } from "@earendil-works/pi-coding-agent";
5968
+ var CHOICES = /* @__PURE__ */ new Map([
5969
+ ["Trust", { trusted: true, save: true }],
5970
+ ["Trust (this session only)", { trusted: true, save: false }],
5971
+ ["Do not trust", { trusted: false, save: true }],
5972
+ ["Do not trust (this session only)", { trusted: false, save: false }]
5973
+ ]);
5974
+ function projectTrustDecision(agentDir, cwd, piTrusted) {
5975
+ if (!piTrusted) return false;
5976
+ if (hasTrustRequiringProjectResources(cwd)) return true;
5977
+ const saved = new ProjectTrustStore(agentDir).get(cwd);
5978
+ if (saved !== null) return saved;
5979
+ const policy = SettingsManager.create(cwd, agentDir, { projectTrusted: false }).getDefaultProjectTrust();
5980
+ return policy === "ask" ? void 0 : policy === "always";
5981
+ }
5982
+ async function askProjectTrust(agentDir, cwd, select, signal) {
5983
+ const answer = await select(
5984
+ `Trust project folder?
5985
+ ${cwd}
5986
+
5987
+ Its .mcp.json defines MCP servers, which can run local commands with your permissions. Saved decisions use Pi's project trust and also apply to Pi project resources.`,
5988
+ [...CHOICES.keys()],
5989
+ { signal }
5990
+ );
5991
+ const choice = answer === void 0 ? void 0 : CHOICES.get(answer);
5992
+ if (!choice || signal.aborted) return void 0;
5993
+ if (choice.save) new ProjectTrustStore(agentDir).set(cwd, choice.trusted);
5994
+ return choice.trusted;
5995
+ }
5996
+
5961
5997
  // src/index.ts
5962
5998
  var CommandUsageError = class extends Error {
5963
5999
  };
@@ -5986,6 +6022,8 @@ function mcpClient(pi, options = {}) {
5986
6022
  let loginController;
5987
6023
  let promptController;
5988
6024
  let importController;
6025
+ let trustController;
6026
+ let sessionTrust;
5989
6027
  pi.registerEntryRenderer(
5990
6028
  STATUS_ENTRY,
5991
6029
  (entry, _options, theme) => statusPanel(entry.data ?? { servers: [], loaded: [] }, theme)
@@ -6053,6 +6091,35 @@ Text output is limited to 2000 lines or 50 KiB; larger results are saved to a pr
6053
6091
  }
6054
6092
  });
6055
6093
  }
6094
+ const trustDecision = (ctx) => projectTrustDecision(agentDir, ctx.cwd, ctx.isProjectTrusted()) ?? (sessionTrust?.cwd === ctx.cwd ? sessionTrust.trusted : void 0);
6095
+ const projectTrusted = (ctx) => trustDecision(ctx) ?? false;
6096
+ async function resolveProjectTrust(ctx) {
6097
+ let trusted = trustDecision(ctx);
6098
+ if (!existsSync(join4(ctx.cwd, ".mcp.json"))) return trusted ?? false;
6099
+ let answered = false;
6100
+ if (trusted === void 0 && ctx.hasUI) {
6101
+ const controller = new AbortController();
6102
+ trustController = controller;
6103
+ try {
6104
+ trusted = await askProjectTrust(
6105
+ agentDir,
6106
+ ctx.cwd,
6107
+ ctx.ui.select.bind(ctx.ui),
6108
+ AbortSignal.any([controller.signal, ...ctx.signal ? [ctx.signal] : []])
6109
+ );
6110
+ } finally {
6111
+ if (trustController === controller) trustController = void 0;
6112
+ }
6113
+ if (controller.signal.aborted) return false;
6114
+ if (trusted !== void 0) {
6115
+ sessionTrust = { cwd: ctx.cwd, trusted };
6116
+ answered = true;
6117
+ }
6118
+ }
6119
+ if (!trusted && !answered && ctx.hasUI && ctx.isProjectTrusted())
6120
+ ctx.ui.notify("Project .mcp.json ignored: this folder isn't trusted for MCP servers. Run /trust to save a decision, then /mcp reload.", "warning");
6121
+ return trusted ?? false;
6122
+ }
6056
6123
  const restore = (ctx) => {
6057
6124
  const tools = restoredTools(ctx.sessionManager.getBranch()).filter((tool) => {
6058
6125
  try {
@@ -6087,14 +6154,15 @@ Text output is limited to 2000 lines or 50 KiB; larger results are saved to a pr
6087
6154
  }
6088
6155
  };
6089
6156
  ctx.signal?.throwIfAborted();
6157
+ const trusted = mutation ? projectTrusted(ctx) : await resolveProjectTrust(ctx);
6090
6158
  const update = mutation && await updateServerConfig(
6091
6159
  agentDir,
6092
6160
  ctx.cwd,
6093
- ctx.isProjectTrusted(),
6161
+ trusted,
6094
6162
  mutation,
6095
6163
  validate
6096
6164
  );
6097
- const nextConfig = update ? update.config : await loadConfig(agentDir, ctx.cwd, ctx.isProjectTrusted());
6165
+ const nextConfig = update ? update.config : await loadConfig(agentDir, ctx.cwd, trusted);
6098
6166
  validate(nextConfig);
6099
6167
  const next = new McpRuntime(
6100
6168
  nextConfig,
@@ -6122,15 +6190,18 @@ Text output is limited to 2000 lines or 50 KiB; larger results are saved to a pr
6122
6190
  return update?.scope;
6123
6191
  }
6124
6192
  pi.on("session_start", async (_event, ctx) => {
6125
- sessionGeneration++;
6193
+ const generation = ++sessionGeneration;
6126
6194
  promptController?.abort();
6127
6195
  importController?.abort();
6196
+ trustController?.abort();
6128
6197
  await runtime?.close();
6129
6198
  runtime = void 0;
6130
6199
  config = {};
6131
6200
  configError = void 0;
6132
6201
  try {
6133
- config = await loadConfig(agentDir, ctx.cwd, ctx.isProjectTrusted());
6202
+ const trusted = await resolveProjectTrust(ctx);
6203
+ if (generation !== sessionGeneration) return;
6204
+ config = await loadConfig(agentDir, ctx.cwd, trusted);
6134
6205
  runtime = new McpRuntime(config, ctx.cwd, join4(agentDir, "cache", "pi-mcp-client"), createSdkConnector(storeFactory));
6135
6206
  resourceNotifications(runtime, ctx);
6136
6207
  } catch (error) {
@@ -6150,6 +6221,7 @@ Text output is limited to 2000 lines or 50 KiB; larger results are saved to a pr
6150
6221
  loginController?.abort();
6151
6222
  promptController?.abort();
6152
6223
  importController?.abort();
6224
+ trustController?.abort();
6153
6225
  const old = runtime;
6154
6226
  runtime = void 0;
6155
6227
  await old?.close();
@@ -6419,6 +6491,7 @@ Variables: ${(candidate.variables ?? []).join(", ") || "none"}. Supply known val
6419
6491
  args,
6420
6492
  ctx,
6421
6493
  agentDir,
6494
+ () => projectTrusted(ctx),
6422
6495
  AbortSignal.any([controller.signal, ...ctx.signal ? [ctx.signal] : []]),
6423
6496
  () => {
6424
6497
  if (sessionGeneration !== generation || runtime !== activeRuntime)
@@ -6478,7 +6551,7 @@ ${snapshot.body}`,
6478
6551
  server = mutation.server;
6479
6552
  await reloadConfiguration(ctx, mutation);
6480
6553
  const remaining = Object.hasOwn(config, server);
6481
- const message = mutation.action === "add" ? `\u2714\uFE0E ${server}: saved in ${mutation.scope} configuration. Connections, authentication, and tool discovery remain on demand.` + (mutation.scope === "global" && ctx.isProjectTrusted() ? " Trusted project definitions take precedence over global definitions." : "") : `\u2714\uFE0E ${server}: removed from ${mutation.scope} configuration. Credentials were retained.` + (remaining ? " A definition from the other scope remains effective; connections reopen on demand." : " Its connection is closed and its tools are no longer active.");
6554
+ const message = mutation.action === "add" ? `\u2714\uFE0E ${server}: saved in ${mutation.scope} configuration. Connections, authentication, and tool discovery remain on demand.` + (mutation.scope === "global" && projectTrusted(ctx) ? " Trusted project definitions take precedence over global definitions." : "") : `\u2714\uFE0E ${server}: removed from ${mutation.scope} configuration. Credentials were retained.` + (remaining ? " A definition from the other scope remains effective; connections reopen on demand." : " Its connection is closed and its tools are no longer active.");
6482
6555
  if (ctx.hasUI) ctx.ui.notify(message, "info");
6483
6556
  return;
6484
6557
  }
package/docs/behavior.md CHANGED
@@ -125,7 +125,8 @@ see [large results](troubleshooting.md#large-results) for size limits.
125
125
 
126
126
  Only load configuration you trust. Server executables and secret commands run
127
127
  with your user permissions; trusted project configuration can replace global
128
- connections and settings.
128
+ connections and settings. See [project trust](#project-trust) for when a
129
+ project's `.mcp.json` loads.
129
130
 
130
131
  Configuration imports require an explicit file, scope, selection, and final
131
132
  confirmation. Import previews hide connection values; review the source file
@@ -148,3 +149,28 @@ Pi's tool restrictions to prevent discovery and resource operations. Already act
148
149
  native tools have their own tool restrictions. Per-resource permission policies
149
150
  aren't implemented. Prompt selection uses explicit user commands, not the
150
151
  model-facing tool allowlist; disable the server to prevent prompt access.
152
+
153
+ ## Project trust
154
+
155
+ A project's `.mcp.json` can start local commands, so it loads only after an
156
+ explicit trust decision for the folder. Pi asks for trust only in folders with Pi
157
+ project resources, such as `.pi/settings.json` or `.agents/skills`, and trusts
158
+ other folders implicitly. The extension doesn't rely on that implicit trust:
159
+
160
+ - **Folders with Pi project resources:** Pi's decision applies, including
161
+ `--approve` and session-only trust.
162
+ - **Other folders:** a saved decision for the folder or a parent folder applies.
163
+ Without one, Pi's `defaultProjectTrust` setting applies: `always` loads project
164
+ servers and `never` ignores them. With the default `ask`, interactive sessions
165
+ ask when a `.mcp.json` exists, and headless sessions ignore the file.
166
+ - **Untrusted folders:** `--no-approve` or a refusal in Pi always ignores project
167
+ servers.
168
+
169
+ Saved answers go to Pi's project trust store, so they also apply to Pi project
170
+ resources. Use Pi's `/trust` command to save or change a decision, then run
171
+ `/mcp reload`; MCP servers don't require a restart. In folders without Pi project
172
+ resources, `--approve` doesn't trust `.mcp.json`; save a decision instead.
173
+
174
+ Only session start and `/mcp reload` ask. Other configuration commands, such as
175
+ `/mcp add` and `/mcp import`, use the current decision. Untrusted project files
176
+ are neither read nor changed.
package/docs/commands.md CHANGED
@@ -120,8 +120,9 @@ capabilities you need.
120
120
  Both commands require an explicit `--scope global` or `--scope project`:
121
121
 
122
122
  - **Global:** `~/.pi/agent/mcp.json`.
123
- - **Project:** `.mcp.json` in the current trusted project. Untrusted project files
124
- are neither read nor changed.
123
+ - **Project:** `.mcp.json` in the current
124
+ [trusted project](behavior.md#project-trust). Untrusted project files are
125
+ neither read nor changed.
125
126
 
126
127
  Add an HTTP server by URL, or a stdio server after `--`:
127
128
 
@@ -194,7 +195,8 @@ automatically; no conversion file is needed:
194
195
  ```
195
196
 
196
197
  The command requires an interactive TUI or RPC session and an explicit
197
- `--scope global` or `--scope project`. Project scope requires a trusted project.
198
+ `--scope global` or `--scope project`. Project scope requires a
199
+ [trusted project](behavior.md#project-trust).
198
200
  Relative source paths resolve against Pi's current directory; `~/` is supported.
199
201
  The path isn't evaluated by a shell, and no application settings are scanned.
200
202
 
@@ -3,9 +3,10 @@
3
3
  [Back to the README](../README.md)
4
4
 
5
5
  You configure which servers the model can access. Add connections to
6
- `~/.pi/agent/mcp.json`, or `.mcp.json` in a trusted project. For command-based
7
- setup, see [Add and remove servers](commands.md#add-and-remove-servers). To reuse
8
- an existing Claude/Cursor JSON or Codex TOML file, see
6
+ `~/.pi/agent/mcp.json`, or `.mcp.json` in a
7
+ [trusted project](behavior.md#project-trust). For command-based setup, see
8
+ [Add and remove servers](commands.md#add-and-remove-servers). To reuse an
9
+ existing Claude/Cursor JSON or Codex TOML file, see
9
10
  [Import server definitions](commands.md#import-server-definitions).
10
11
 
11
12
  ## Files and transports
@@ -16,8 +17,10 @@ MCP configuration standard. Live configuration must use JSON, not VS Code's
16
17
  this format. `PI_CODING_AGENT_DIR` overrides the global Pi directory.
17
18
  Project definitions replace same-named global definitions in full; fields and
18
19
  filters aren't merged. Untrusted project definitions aren't loaded or edited.
19
- An explicitly named import source is read as data for review; saving into project
20
- scope still requires project trust.
20
+ Project servers need an explicit trust decision even in folders that Pi trusts
21
+ implicitly; see [project trust](behavior.md#project-trust). An explicitly named
22
+ import source is read as data for review; saving into project scope still
23
+ requires project trust.
21
24
 
22
25
  ```json
23
26
  {
@@ -44,13 +47,24 @@ scope still requires project trust.
44
47
  | `type` | Optional `stdio` or `http`. If omitted, inferred from `command` or `url`. A conflicting type is rejected. |
45
48
  | `command`, `args` | Executable and arguments for a stdio server. No shell is used. |
46
49
  | `cwd` | Working directory for stdio; defaults to Pi's current directory. Relative paths resolve there. |
47
- | `env` | Additional environment variables for stdio. |
50
+ | `env` | Environment variables for stdio, in addition to a minimal inherited set. |
48
51
  | `url` | Streamable HTTP endpoint; mutually exclusive with `command`. |
49
52
  | `headers` | HTTP request headers, including optional bearer authentication. |
50
53
 
51
54
  Strings in `command`, `args`, `cwd`, `env`, `url`, and `headers` support `${VAR}`
52
55
  interpolation. Missing variables prevent that server from connecting.
53
56
 
57
+ Stdio servers don't inherit Pi's environment. They receive only the MCP SDK's
58
+ default set (`HOME`, `LOGNAME`, `PATH`, `SHELL`, `TERM`, and `USER` on Unix, and
59
+ similar system variables on Windows) plus `env`. Pass other variables a server
60
+ needs explicitly, for example a custom browser location:
61
+
62
+ ```json
63
+ "env": {
64
+ "PLAYWRIGHT_BROWSERS_PATH": "${PLAYWRIGHT_BROWSERS_PATH}"
65
+ }
66
+ ```
67
+
54
68
  Only stdio and Streamable HTTP are supported. The extension rejects `type: "sse"`
55
69
  and unsupported connection fields rather than silently changing their meaning.
56
70
 
@@ -69,6 +69,21 @@ remain visible as content, even when the tool reports an error; they aren't
69
69
  sanitized transport diagnostics. Tool-call failures aren't replayed automatically;
70
70
  verify the outcome before retrying.
71
71
 
72
+ ## Missing project servers
73
+
74
+ A project's `.mcp.json` loads only after an explicit trust decision, even in
75
+ folders that Pi otherwise trusts implicitly. Headless sessions ignore it when no
76
+ decision exists. Run Pi's `/trust` command to save a decision, then run
77
+ `/mcp reload`. See [project trust](behavior.md#project-trust).
78
+
79
+ ## Missing server dependencies
80
+
81
+ If a stdio server reports a missing browser, SDK, credential, or configuration
82
+ file that works in your shell, it probably reads an environment variable that
83
+ it doesn't receive. Servers don't inherit Pi's environment; pass the variable
84
+ through `env`, for example `PLAYWRIGHT_BROWSERS_PATH`. See
85
+ [files and transports](configuration.md#files-and-transports).
86
+
72
87
  ## Large results
73
88
 
74
89
  Text output is limited to 2,000 lines or 50 KiB, including resource reads and
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-mcp-client",
3
- "version": "0.8.0",
3
+ "version": "0.8.1",
4
4
  "description": "MCP tools for Pi, discovered on demand and called natively through the official SDK.",
5
5
  "type": "module",
6
6
  "license": "MIT",