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 +3 -1
- package/dist/index.js +83 -10
- package/docs/behavior.md +27 -1
- package/docs/commands.md +5 -3
- package/docs/configuration.md +20 -6
- package/docs/troubleshooting.md +15 -0
- package/package.json +1 -1
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
|
|
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
|
|
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 =
|
|
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
|
|
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 (
|
|
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
|
-
|
|
6161
|
+
trusted,
|
|
6094
6162
|
mutation,
|
|
6095
6163
|
validate
|
|
6096
6164
|
);
|
|
6097
|
-
const nextConfig = update ? update.config : await loadConfig(agentDir, ctx.cwd,
|
|
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
|
-
|
|
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
|
|
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
|
|
124
|
-
|
|
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
|
|
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
|
|
package/docs/configuration.md
CHANGED
|
@@ -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
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
20
|
-
|
|
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` |
|
|
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
|
|
package/docs/troubleshooting.md
CHANGED
|
@@ -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
|