@piagent/platform 1.1.9 → 1.2.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/CHANGELOG.md +175 -1
- package/README.md +2 -2
- package/docs/command-reference-vietnamese.md +19 -5
- package/docs/distribution-standard.md +7 -7
- package/docs/mcp-and-tools.md +154 -2
- package/docs/model-options.md +10 -8
- package/docs/operator-manual-vietnamese.md +25 -13
- package/docs/publishing-for-teams.md +2 -0
- package/docs/quickstart-vietnamese.md +2 -2
- package/docs/release-install-policy.md +6 -6
- package/docs/security-threat-model.md +4 -3
- package/docs/team-onboarding.md +4 -4
- package/docs/usage-observability.md +29 -0
- package/docs/vercel-docs-site.md +34 -4
- package/docs-site/architecture.html +152 -0
- package/docs-site/assets/docs.css +1001 -0
- package/docs-site/assets/docs.js +84 -0
- package/docs-site/commands.html +203 -0
- package/docs-site/content/architecture.html +29 -0
- package/docs-site/content/commands.html +80 -0
- package/docs-site/content/index.html +80 -0
- package/docs-site/content/mcp-approval.html +133 -0
- package/docs-site/content/mcp-auth.html +76 -0
- package/docs-site/content/mcp-servers.html +111 -0
- package/docs-site/content/mcp.html +120 -0
- package/docs-site/content/permissions.html +54 -0
- package/docs-site/content/profiles.html +68 -0
- package/docs-site/content/quickstart.html +144 -0
- package/docs-site/content/runtime.html +45 -0
- package/docs-site/content/scope.html +71 -0
- package/docs-site/content/security.html +51 -0
- package/docs-site/content/team.html +23 -0
- package/docs-site/content/workflows.html +47 -0
- package/docs-site/index.html +131 -1534
- package/docs-site/mcp-approval.html +256 -0
- package/docs-site/mcp-auth.html +199 -0
- package/docs-site/mcp-servers.html +234 -0
- package/docs-site/mcp.html +243 -0
- package/docs-site/permissions.html +177 -0
- package/docs-site/profiles.html +191 -0
- package/docs-site/quickstart.html +267 -0
- package/docs-site/runtime.html +168 -0
- package/docs-site/scope.html +194 -0
- package/docs-site/security.html +174 -0
- package/docs-site/team.html +142 -0
- package/docs-site/workflows.html +170 -0
- package/package.json +4 -2
- package/packages/piagent-core/capabilities/capability-core.js +58 -5
- package/packages/piagent-core/capabilities/capability-sources.js +73 -12
- package/packages/piagent-core/extensions/document-intake.ts +5 -5
- package/packages/piagent-core/extensions/piagent-guard.ts +432 -2
- package/packages/piagent-core/extensions/policy-core.js +1586 -98
- package/packages/piagent-core/mcp/mcp-approval-store.js +216 -0
- package/packages/piagent-core/mcp/mcp-auth-readiness.js +225 -0
- package/packages/piagent-core/mcp/mcp-command-actions.js +400 -0
- package/packages/piagent-core/mcp/mcp-config-layers.js +466 -0
- package/packages/piagent-core/mcp/mcp-import-kinds.js +240 -0
- package/packages/piagent-core/mcp/mcp-server-catalog.js +150 -0
- package/packages/piagent-core/mcp/mcp-server-entry.js +293 -0
- package/packages/piagent-core/mcp/mcp-session-view.js +236 -0
- package/packages/piagent-core/mcp/mcp-tool-naming.js +90 -0
- package/packages/piagent-core/package.json +7 -3
- package/packages/piagent-core/prompts/model-options.md +8 -8
- package/scripts/build-docs-site.mjs +421 -0
- package/scripts/configure-model-scope.sh +21 -5
- package/scripts/configure-subagents.sh +18 -5
- package/scripts/init-project.sh +15 -8
- package/scripts/install-global.sh +5 -5
- package/scripts/mcp-manage.mjs +656 -0
- package/scripts/pi-model-catalog.sh +11 -1
- package/scripts/pi-session-stats.sh +27 -1
- package/scripts/pi-usage-history.mjs +567 -0
- package/scripts/piagent-cli.mjs +1 -1
- package/scripts/preview-docs-site.mjs +88 -0
- package/scripts/quality-benchmark.sh +37 -9
- package/scripts/setup.sh +23 -15
- package/scripts/team-doctor.sh +18 -2
- package/scripts/uninstall-global.sh +10 -4
- package/scripts/verify-local.sh +65 -19
- package/scripts/verify-release-identity.mjs +37 -2
- package/scripts/configure-mcp.sh +0 -349
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
import {
|
|
2
|
+
REPOSITORY_SCOPES,
|
|
3
|
+
collectServers,
|
|
4
|
+
isRecord,
|
|
5
|
+
mergedServerEntry,
|
|
6
|
+
readMcpConfig,
|
|
7
|
+
writeMcpConfig
|
|
8
|
+
} from "./mcp-config-layers.js";
|
|
9
|
+
import { approvalStorePath, approvalState, clearApproval, recordApproval } from "./mcp-approval-store.js";
|
|
10
|
+
import { maskServerEntry } from "./mcp-server-entry.js";
|
|
11
|
+
import {
|
|
12
|
+
McpViewError,
|
|
13
|
+
collectServerRows,
|
|
14
|
+
describeServer,
|
|
15
|
+
formatDoctorReport,
|
|
16
|
+
formatServerDetail,
|
|
17
|
+
formatServerTable,
|
|
18
|
+
sessionMcpState
|
|
19
|
+
} from "./mcp-session-view.js";
|
|
20
|
+
|
|
21
|
+
// What `/piagent-mcp` does, separated from how a session shows it.
|
|
22
|
+
//
|
|
23
|
+
// Each action takes plain options and returns a report: a one-line notice, the
|
|
24
|
+
// lines to display, and the structured detail behind them. Nothing here knows
|
|
25
|
+
// about `pi`, a UI, or a terminal, so the menu path and the typed-subcommand
|
|
26
|
+
// path cannot drift into doing different things — they call the same function.
|
|
27
|
+
|
|
28
|
+
export { McpViewError };
|
|
29
|
+
|
|
30
|
+
/** Actions that only read. The rest write a config file or an approval. */
|
|
31
|
+
export const READ_ACTIONS = new Set(["status", "get", "doctor"]);
|
|
32
|
+
|
|
33
|
+
/** Actions that name a single server. `reset` accepts one but does not need it. */
|
|
34
|
+
export const SERVER_ACTIONS = new Set(["get", "approve", "reject", "reset", "enable", "disable"]);
|
|
35
|
+
|
|
36
|
+
/** Writes that belong in a shell, with the command to run there. */
|
|
37
|
+
export const TERMINAL_ONLY = new Map([
|
|
38
|
+
["add", "piagent-mcp add <name> [--scope <scope>] --url <url> | -- <command> [args...]"],
|
|
39
|
+
["remove", "piagent-mcp remove <name> --scope <scope>"],
|
|
40
|
+
["preset", "piagent-mcp --preset <core|popular|design> --scope <scope>"]
|
|
41
|
+
]);
|
|
42
|
+
|
|
43
|
+
export const HELP_LINES = [
|
|
44
|
+
"/piagent-mcp open the menu",
|
|
45
|
+
"/piagent-mcp status every configured server, with scope and readiness",
|
|
46
|
+
"/piagent-mcp get <name> one server in detail, credential values masked",
|
|
47
|
+
"/piagent-mcp doctor what each server still needs before it can answer",
|
|
48
|
+
"/piagent-mcp approve <name> allow a server this repository defines",
|
|
49
|
+
"/piagent-mcp reject <name> refuse it",
|
|
50
|
+
"/piagent-mcp reset [name] forget the decision, or every decision",
|
|
51
|
+
"/piagent-mcp enable|disable <name>",
|
|
52
|
+
"",
|
|
53
|
+
"Adding, removing and presets are terminal commands: run `piagent-mcp --help`.",
|
|
54
|
+
"Live connection state and OAuth sign-in belong to the adapter: /mcp and /mcp-auth."
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The menu, built from what this project actually has.
|
|
59
|
+
*
|
|
60
|
+
* An entry is offered only when it would do something: approving is not offered
|
|
61
|
+
* when no server here needs a decision, and a menu that lists impossible actions
|
|
62
|
+
* teaches the reader to stop reading it. Counts go in the labels so the menu
|
|
63
|
+
* answers part of the question before anything is picked.
|
|
64
|
+
*
|
|
65
|
+
* @param {{projectPath: string}} options
|
|
66
|
+
*/
|
|
67
|
+
export function menuOptions(options) {
|
|
68
|
+
const state = sessionMcpState({ projectPath: options.projectPath });
|
|
69
|
+
const { rows, blocked: stuck } = state;
|
|
70
|
+
// Not `REPOSITORY_SCOPES.has(row.scope)`: a repository-relative import
|
|
71
|
+
// declared by a `global` config produces a row whose scope is `global` and
|
|
72
|
+
// whose definition came out of the clone. Those were the rows the menu never
|
|
73
|
+
// offered to approve, so the only route to a decision was knowing to type the
|
|
74
|
+
// subcommand -- for exactly the servers whose origin is hardest to see.
|
|
75
|
+
const repositoryRows = rows.filter((row) => row.requiresApproval);
|
|
76
|
+
const decisions = repositoryRows.map((row) => ({
|
|
77
|
+
row,
|
|
78
|
+
state: approvalState({ projectPath: options.projectPath, name: row.name, entry: row.entry }).state
|
|
79
|
+
}));
|
|
80
|
+
const undecided = decisions.filter((item) => item.state !== "approved");
|
|
81
|
+
const decided = decisions.filter((item) => item.state !== "pending");
|
|
82
|
+
const disabled = rows.filter((row) => row.readiness.state === "disabled");
|
|
83
|
+
const enabled = rows.filter((row) => row.readiness.state !== "disabled");
|
|
84
|
+
|
|
85
|
+
const entries = [
|
|
86
|
+
{
|
|
87
|
+
value: "status",
|
|
88
|
+
label: state.usable
|
|
89
|
+
? (rows.length === 0 ? "Servers — none configured" : `Servers — ${rows.length - stuck.length}/${rows.length} ready`)
|
|
90
|
+
: "Servers — every tool call blocked",
|
|
91
|
+
description: state.usable
|
|
92
|
+
? "Every server visible from this project, with its scope and state"
|
|
93
|
+
: "Config here leaves nothing to check an approval against"
|
|
94
|
+
}
|
|
95
|
+
];
|
|
96
|
+
// Ahead of everything per-server: while this stands, no decision about an
|
|
97
|
+
// individual server changes whether anything runs.
|
|
98
|
+
if (!state.usable) {
|
|
99
|
+
entries.push({
|
|
100
|
+
value: "doctor",
|
|
101
|
+
label: `Doctor — ${state.blockingConfig.length} config problem${state.blockingConfig.length === 1 ? "" : "s"} blocking every call`,
|
|
102
|
+
description: "Which file to edit before any server can answer",
|
|
103
|
+
recommended: true
|
|
104
|
+
});
|
|
105
|
+
} else if (stuck.length > 0) {
|
|
106
|
+
entries.push({
|
|
107
|
+
value: "doctor",
|
|
108
|
+
label: `Doctor — ${stuck.length} need attention`,
|
|
109
|
+
description: "What each blocked server still needs",
|
|
110
|
+
recommended: true
|
|
111
|
+
});
|
|
112
|
+
} else if (rows.length > 0) {
|
|
113
|
+
entries.push({ value: "doctor", label: "Doctor — all reachable", description: "Re-check what each server needs" });
|
|
114
|
+
}
|
|
115
|
+
if (rows.length > 0) {
|
|
116
|
+
entries.push({ value: "get", label: "Inspect a server", description: "Full definition, credential values masked" });
|
|
117
|
+
}
|
|
118
|
+
if (undecided.length > 0) {
|
|
119
|
+
entries.push({
|
|
120
|
+
value: "approve",
|
|
121
|
+
label: `Approve a repository server — ${undecided.length} waiting`,
|
|
122
|
+
description: "This repository defines it; nobody on this machine has allowed it yet",
|
|
123
|
+
recommended: stuck.length === 0
|
|
124
|
+
});
|
|
125
|
+
entries.push({ value: "reject", label: "Reject a repository server", description: "Refuse it for this project" });
|
|
126
|
+
} else if (repositoryRows.length > 0) {
|
|
127
|
+
entries.push({ value: "reject", label: "Reject a repository server", description: "Withdraw a server this project had allowed" });
|
|
128
|
+
}
|
|
129
|
+
if (decided.length > 0) {
|
|
130
|
+
entries.push({ value: "reset", label: "Forget a decision", description: "Return a server to pending so it is asked again" });
|
|
131
|
+
}
|
|
132
|
+
if (enabled.length > 0) {
|
|
133
|
+
entries.push({ value: "disable", label: "Turn a server off", description: "Stop using it without losing its definition" });
|
|
134
|
+
}
|
|
135
|
+
if (disabled.length > 0) {
|
|
136
|
+
entries.push({ value: "enable", label: `Turn a server back on — ${disabled.length} off`, description: "Restore a disabled server" });
|
|
137
|
+
}
|
|
138
|
+
entries.push({ value: "add", label: "Add a server", description: "Shows the terminal command; writing config is not a slash command" });
|
|
139
|
+
entries.push({ value: "help", label: "All subcommands", description: "Everything /piagent-mcp accepts" });
|
|
140
|
+
return { entries, rows };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Servers an action can be applied to. Approving a server the repository does
|
|
145
|
+
* not define is not a thing, so it is not offered.
|
|
146
|
+
*
|
|
147
|
+
* @param {{projectPath: string, action: string}} options
|
|
148
|
+
*/
|
|
149
|
+
export function serverChoices(options) {
|
|
150
|
+
const { projectPath, action } = options;
|
|
151
|
+
const rows = collectServerRows({ projectPath });
|
|
152
|
+
const filtered = rows.filter((row) => {
|
|
153
|
+
if (action === "approve" || action === "reject" || action === "reset") return row.requiresApproval;
|
|
154
|
+
// A server reached through `imports` lives in a file this command does not
|
|
155
|
+
// own. Offering it here produced a write against the declaring layer that
|
|
156
|
+
// reported success and changed nothing the adapter reads.
|
|
157
|
+
if (action === "enable" || action === "disable") {
|
|
158
|
+
if (row.origin?.startsWith("import:")) return false;
|
|
159
|
+
}
|
|
160
|
+
if (action === "enable") return row.readiness.state === "disabled";
|
|
161
|
+
if (action === "disable") return row.readiness.state !== "disabled";
|
|
162
|
+
return true;
|
|
163
|
+
});
|
|
164
|
+
return filtered.map((row) => ({
|
|
165
|
+
value: row.name,
|
|
166
|
+
label: `${row.name} (${row.scope})`,
|
|
167
|
+
description: row.readiness.detail
|
|
168
|
+
}));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** @param {{projectPath: string, scope?: string}} options */
|
|
172
|
+
export function status(options) {
|
|
173
|
+
const state = sessionMcpState({
|
|
174
|
+
projectPath: options.projectPath,
|
|
175
|
+
scopes: options.scope ? [options.scope] : undefined
|
|
176
|
+
});
|
|
177
|
+
const { rows, blocked: stuck } = state;
|
|
178
|
+
// Reported before the per-server table, and it decides the headline. A status
|
|
179
|
+
// that says "0 servers configured" in a session where every tool call is being
|
|
180
|
+
// refused is not a smaller version of the truth, it points at the wrong file.
|
|
181
|
+
const blocking = state.blockingConfig.map((problem) => `BLOCKED every tool call: ${problem.detail}`);
|
|
182
|
+
const summary = !state.usable
|
|
183
|
+
? "MCP: every tool call blocked by config"
|
|
184
|
+
: rows.length === 0
|
|
185
|
+
? "MCP: no servers configured"
|
|
186
|
+
: `MCP: ${rows.length - stuck.length}/${rows.length} ready`;
|
|
187
|
+
return {
|
|
188
|
+
notify: { message: summary, level: state.usable && stuck.length === 0 ? "info" : "warning" },
|
|
189
|
+
lines: [
|
|
190
|
+
...blocking,
|
|
191
|
+
...(blocking.length > 0 ? [""] : []),
|
|
192
|
+
...(rows.length === 0
|
|
193
|
+
? (state.usable
|
|
194
|
+
? ["No MCP servers configured.", "Seed the pinned baseline in a terminal: piagent-mcp --preset core --scope global"]
|
|
195
|
+
: [])
|
|
196
|
+
: [formatServerTable(rows), ...(stuck.length > 0 ? ["", `${stuck.length} not usable yet. Run /piagent-mcp doctor.`] : [])])
|
|
197
|
+
],
|
|
198
|
+
details: {
|
|
199
|
+
project: options.projectPath,
|
|
200
|
+
usable: state.usable,
|
|
201
|
+
blockingConfig: state.blockingConfig,
|
|
202
|
+
servers: rows.map((row) => ({
|
|
203
|
+
name: row.name,
|
|
204
|
+
scope: row.scope,
|
|
205
|
+
origin: row.origin,
|
|
206
|
+
requiresApproval: row.requiresApproval,
|
|
207
|
+
state: row.readiness.state,
|
|
208
|
+
detail: row.readiness.detail
|
|
209
|
+
}))
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** @param {{projectPath: string, name: string, scope?: string}} options */
|
|
215
|
+
export function detail(options) {
|
|
216
|
+
const found = describeServer({
|
|
217
|
+
projectPath: options.projectPath,
|
|
218
|
+
name: options.name,
|
|
219
|
+
scope: options.scope,
|
|
220
|
+
listHint: "/piagent-mcp",
|
|
221
|
+
scopeHint: "--scope"
|
|
222
|
+
});
|
|
223
|
+
return {
|
|
224
|
+
notify: { message: `MCP ${options.name}: ${found.readiness.state}`, level: "info" },
|
|
225
|
+
lines: formatServerDetail(found, { inSession: true }),
|
|
226
|
+
details: { name: options.name, scope: found.scope, file: found.file, state: found.readiness.state, entry: found.masked }
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* The Docker daemon probe is deliberately absent: it spawns a process, and this
|
|
232
|
+
* runs inside the session's own turn. `piagent-mcp doctor` in a terminal does it.
|
|
233
|
+
*
|
|
234
|
+
* @param {{projectPath: string}} options
|
|
235
|
+
*/
|
|
236
|
+
export function doctor(options) {
|
|
237
|
+
const state = sessionMcpState({ projectPath: options.projectPath });
|
|
238
|
+
const report = formatDoctorReport(state.rows, { rerun: "/piagent-mcp doctor", inSession: true });
|
|
239
|
+
// A layer nobody can parse counts as a problem: it is hiding whatever it
|
|
240
|
+
// defines. An import declaration does not — the servers it brings in are in
|
|
241
|
+
// the table above with their own approval state — but it is named, because
|
|
242
|
+
// reading the repository's config alone would not reveal where they came from.
|
|
243
|
+
const problems = report.problems + state.unreadable.length + state.blockingConfig.length;
|
|
244
|
+
return {
|
|
245
|
+
notify: {
|
|
246
|
+
message: problems === 0 ? "MCP: every server can be reached" : `MCP: ${problems} need attention`,
|
|
247
|
+
level: problems === 0 ? "info" : "warning"
|
|
248
|
+
},
|
|
249
|
+
lines: [
|
|
250
|
+
// First. No per-server remedy below is worth acting on while one of these
|
|
251
|
+
// stands, because none of them makes a tool call succeed.
|
|
252
|
+
...state.blockingConfig.map((problem) => `BLOCKED every tool call: ${problem.detail}`),
|
|
253
|
+
...(state.blockingConfig.length > 0 ? [""] : []),
|
|
254
|
+
...report.lines,
|
|
255
|
+
...state.unreadable.map((layer) => `Unreadable ${layer.scope} config: ${layer.detail}`),
|
|
256
|
+
...state.imports.map((layer) =>
|
|
257
|
+
`${layer.file} imports servers from ${layer.kinds.join(", ")} config; ` +
|
|
258
|
+
"they need approval here like any server the repository defines."),
|
|
259
|
+
...(problems === 0 ? [] : ["", "The Docker daemon check is terminal-only: piagent-mcp doctor"])
|
|
260
|
+
],
|
|
261
|
+
details: {
|
|
262
|
+
project: options.projectPath,
|
|
263
|
+
problems,
|
|
264
|
+
usable: state.usable,
|
|
265
|
+
blockingConfig: state.blockingConfig,
|
|
266
|
+
unreadableLayers: state.unreadable.map((layer) => layer.scope),
|
|
267
|
+
repositoryImports: state.imports.map((layer) => ({ scope: layer.scope, kinds: layer.kinds }))
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** @param {{projectPath: string, name: string, decision: "approved"|"rejected"}} options */
|
|
273
|
+
export function decide(options) {
|
|
274
|
+
const { projectPath, name, decision } = options;
|
|
275
|
+
const matches = collectServers({ projectPath, scopes: [...REPOSITORY_SCOPES] }).filter((server) => server.name === name);
|
|
276
|
+
if (matches.length === 0) {
|
|
277
|
+
throw new McpViewError(
|
|
278
|
+
`${name} is not defined by this repository. Approval only applies to servers in ${[...REPOSITORY_SCOPES].join(" or ")} scope.`
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
if (matches.length > 1) {
|
|
282
|
+
throw new McpViewError(`${name} is defined in ${matches.map((server) => server.scope).join(" and ")}; remove one.`);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const server = matches[0];
|
|
286
|
+
const before = approvalState({ projectPath, name, entry: server.entry });
|
|
287
|
+
// Approving without reading it is the failure this gate exists to prevent, so
|
|
288
|
+
// the definition is put in front of the operator alongside the decision.
|
|
289
|
+
//
|
|
290
|
+
// What is shown is the merged entry, not this layer's fragment. The adapter
|
|
291
|
+
// merges a same-named server key by key across layers, so a repository that
|
|
292
|
+
// declares only `{"args": ["--evil"]}` gets the command from a lower layer --
|
|
293
|
+
// and a preview of the fragment showed the operator an argument list with no
|
|
294
|
+
// command attached while consenting to a full command line.
|
|
295
|
+
const preview = decision === "approved" && before.state !== "approved"
|
|
296
|
+
? [JSON.stringify(maskServerEntry(mergedServerEntry({ projectPath, name, entry: server.entry })), null, 2), ""]
|
|
297
|
+
: [];
|
|
298
|
+
// A decision that was not written is not a decision. Reporting success here
|
|
299
|
+
// would leave the operator believing a server is approved while the gate keeps
|
|
300
|
+
// blocking it, or worse, believing one is rejected while nothing recorded it.
|
|
301
|
+
if (!recordApproval({ projectPath, name, entry: server.entry, decision })) {
|
|
302
|
+
throw new McpViewError(
|
|
303
|
+
`could not write ${approvalStorePath() ?? "the approval store"}; ${name} is still ${before.state}`
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
const after = approvalState({ projectPath, name, entry: server.entry });
|
|
307
|
+
return {
|
|
308
|
+
notify: { message: `MCP ${name}: ${decision}`, level: "info" },
|
|
309
|
+
lines: [
|
|
310
|
+
...preview,
|
|
311
|
+
`${decision === "approved" ? "Approved" : "Rejected"} MCP server ${name} (${server.scope}) for ${projectPath}`,
|
|
312
|
+
`Pinned to this definition: ${after.digest}`,
|
|
313
|
+
"Changing the server's command, URL or arguments returns it to pending."
|
|
314
|
+
],
|
|
315
|
+
details: { name, scope: server.scope, decision, digest: after.digest, previous: before.state }
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/** @param {{projectPath: string, name?: string}} options */
|
|
320
|
+
export function reset(options) {
|
|
321
|
+
const { projectPath, name } = options;
|
|
322
|
+
if (!clearApproval({ projectPath, name })) {
|
|
323
|
+
throw new McpViewError(`could not write ${approvalStorePath() ?? "the approval store"}; nothing was forgotten`);
|
|
324
|
+
}
|
|
325
|
+
return {
|
|
326
|
+
notify: { message: name ? `MCP ${name}: decision forgotten` : "MCP: all decisions forgotten", level: "info" },
|
|
327
|
+
lines: [name
|
|
328
|
+
? `Forgot the decision for ${name} in ${projectPath}`
|
|
329
|
+
: `Forgot every MCP approval decision for ${projectPath}`],
|
|
330
|
+
details: { name: name ?? null, decision: "reset" }
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* A server this command has no file of its own to edit: it was read out of
|
|
336
|
+
* another tool's config through `imports`. The write used to land in whichever
|
|
337
|
+
* layer declared the import, where the adapter never looks for it, so the
|
|
338
|
+
* command reported the server disabled and the next tool call still reached it.
|
|
339
|
+
*
|
|
340
|
+
* @param {{name: string, origin?: string, scope: string, file: string}} found
|
|
341
|
+
* @param {string} action
|
|
342
|
+
*/
|
|
343
|
+
function assertOwnedByThisTool(found, action) {
|
|
344
|
+
if (!found.origin?.startsWith("import:")) return;
|
|
345
|
+
const kind = found.origin.slice("import:".length);
|
|
346
|
+
throw new McpViewError(
|
|
347
|
+
`${found.name} is not defined by any config this command owns. The ${found.scope} config imports ${kind}, ` +
|
|
348
|
+
`and the definition lives in ${found.file}. Edit that file to ${action} it, or drop "${kind}" from the imports list.`
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/** @param {{projectPath: string, name: string, scope?: string, enabled: boolean}} options */
|
|
353
|
+
export function toggle(options) {
|
|
354
|
+
const { projectPath, name, scope, enabled } = options;
|
|
355
|
+
const found = describeServer({ projectPath, name, scope, listHint: "/piagent-mcp", scopeHint: "--scope" });
|
|
356
|
+
assertOwnedByThisTool(found, enabled ? "enable" : "disable");
|
|
357
|
+
const config = readMcpConfig(found.file);
|
|
358
|
+
const entry = { ...(isRecord(config.mcpServers[name]) ? config.mcpServers[name] : {}) };
|
|
359
|
+
if (enabled) delete entry.enabled;
|
|
360
|
+
else entry.enabled = false;
|
|
361
|
+
config.mcpServers[name] = entry;
|
|
362
|
+
writeMcpConfig(found.file, config);
|
|
363
|
+
return {
|
|
364
|
+
notify: { message: `MCP ${name}: ${enabled ? "enabled" : "disabled"}`, level: "info" },
|
|
365
|
+
lines: [
|
|
366
|
+
`${enabled ? "Enabled" : "Disabled"} MCP server ${name} in ${found.scope} scope`,
|
|
367
|
+
`File modified: ${found.file}`,
|
|
368
|
+
...(enabled ? [] : ["The definition is kept, so /piagent-mcp enable restores it."]),
|
|
369
|
+
"The adapter reads this at startup: run /reload for it to take effect in this session."
|
|
370
|
+
],
|
|
371
|
+
details: { name, scope: found.scope, file: found.file, enabled }
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/** @param {string} action */
|
|
376
|
+
export function terminalOnly(action) {
|
|
377
|
+
const command = TERMINAL_ONLY.get(action);
|
|
378
|
+
if (!command) return undefined;
|
|
379
|
+
return {
|
|
380
|
+
notify: { message: `MCP: ${action} is a terminal command`, level: "warning" },
|
|
381
|
+
// Why this line sits where it does belongs in the docs. What an operator
|
|
382
|
+
// needs here is the command to run and the reason it is not this one.
|
|
383
|
+
lines: [
|
|
384
|
+
`${action} writes a server definition, so it stays in the terminal:`,
|
|
385
|
+
` ${command}`,
|
|
386
|
+
"",
|
|
387
|
+
"Shell quoting, ${VAR} references and a `--` command line are a shell's job.",
|
|
388
|
+
"Re-reading them here would be a second, worse parser."
|
|
389
|
+
],
|
|
390
|
+
details: { action, command }
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
export function help() {
|
|
395
|
+
return {
|
|
396
|
+
notify: { message: "MCP: subcommands", level: "info" },
|
|
397
|
+
lines: HELP_LINES,
|
|
398
|
+
details: { actions: [...READ_ACTIONS, ...SERVER_ACTIONS] }
|
|
399
|
+
};
|
|
400
|
+
}
|