privateer-agent 0.14.6 → 0.15.0
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 +5 -5
- package/bin/headless-flags.mjs +1 -1
- package/bin/privateer-launch.mjs +33 -0
- package/bin/privateer-subagent.mjs +13 -0
- package/extensions/privateer-codemode.ts +15 -0
- package/extensions/privateer-connect.ts +21 -18
- package/extensions/privateer-web.ts +1 -1
- package/package.json +8 -9
- package/patches/{@earendil-works+pi-ai+0.86.0.patch → @earendil-works+pi-ai+1.3.0.patch} +6 -7
- package/patches/{@earendil-works+pi-coding-agent+0.86.0.patch → @earendil-works+pi-coding-agent+1.3.0.patch} +275 -184
- package/patches/{pi-background-tasks+2.5.0.patch → pi-background-tasks+2.6.10.patch} +36 -21
- package/src/bridge/engineAdapter.ts +6 -0
- package/src/config/moat.ts +51 -12
- package/src/config/moatManifest.json +3 -3
- package/src/config/whatsNew.json +4 -4
- package/src/ext/permissionGate.ts +39 -2
- package/src/harbor/closeSession.ts +1 -1
- package/src/harbor/index.ts +55 -133
- package/src/mcp/catalog.ts +10 -9
- package/src/mcp/piMcp.ts +139 -0
- package/src/mcp/toolNames.ts +39 -140
- package/src/permissions/classify.ts +42 -4
- package/src/permissions/protected.ts +7 -0
- package/src/providers/account.ts +7 -2
- package/src/remote/mcpControl.ts +189 -66
- package/src/remote/relayClient.ts +1 -1
- package/src/routines/toolSelect.ts +4 -4
- package/src/harbor/buildLock.ts +0 -19
package/src/remote/mcpControl.ts
CHANGED
|
@@ -5,13 +5,13 @@
|
|
|
5
5
|
* relay (the harbor today; an interactive terminal by the same shape).
|
|
6
6
|
*
|
|
7
7
|
* The client itself can NEVER run MCP — a browser tab / RN runtime can't spawn a
|
|
8
|
-
* stdio child or hold
|
|
8
|
+
* stdio child or hold a server connection. So "serving MCP to phone/web" means MANAGING the
|
|
9
9
|
* config here, on a host that executes it. This control owns that config.
|
|
10
10
|
*
|
|
11
11
|
* SAME FILE MODEL AS THE DESKTOP (treeview/desktop/src/main/mcpService.ts): the
|
|
12
12
|
* source of truth is `${agentDir}/mcp-desktop.json` — every server with an `enabled`
|
|
13
13
|
* flag — and from it we PROJECT the standard `${agentDir}/mcp.json` (enabled servers
|
|
14
|
-
* only, `{mcpServers:{}}` shape) that
|
|
14
|
+
* only, `{mcpServers:{}}` shape) that Pi's built-in MCP support reads. Sharing those two files
|
|
15
15
|
* means a machine has ONE coherent MCP config whether it was edited from the desktop
|
|
16
16
|
* over IPC or from the phone over the relay.
|
|
17
17
|
*
|
|
@@ -43,15 +43,16 @@ import { writeFileAtomic } from "../util/atomicWrite.ts";
|
|
|
43
43
|
export type McpTransport = "stdio" | "http";
|
|
44
44
|
|
|
45
45
|
/**
|
|
46
|
-
* How an HTTP connector authenticates — our vocabulary,
|
|
47
|
-
*
|
|
48
|
-
* entirely in custom headers); stdio
|
|
46
|
+
* How an HTTP connector authenticates — our vocabulary, kept in mcp-desktop.json and on
|
|
47
|
+
* the relay, and projected onto Pi's config (see toPiServerEntry). "none" is a server that
|
|
48
|
+
* needs no credential at all (or carries one entirely in custom headers); stdio
|
|
49
|
+
* connectors are always "none".
|
|
49
50
|
*/
|
|
50
51
|
export type McpAuth = "oauth" | "bearer" | "none";
|
|
51
52
|
|
|
52
53
|
// One server as stored in the source file (mcp-desktop.json). Mirrors the desktop's
|
|
53
|
-
// SourceEntry: the standard fields
|
|
54
|
-
interface SourceEntry {
|
|
54
|
+
// SourceEntry: the standard server fields plus our auth vocabulary and `enabled` flag.
|
|
55
|
+
export interface SourceEntry {
|
|
55
56
|
transport?: McpTransport;
|
|
56
57
|
command?: string;
|
|
57
58
|
args?: string[];
|
|
@@ -63,7 +64,7 @@ interface SourceEntry {
|
|
|
63
64
|
bearerTokenEnv?: string;
|
|
64
65
|
// LEGACY, read-only: entries written before `auth` existed carry a boolean here.
|
|
65
66
|
// authOf() folds it in; nothing new ever writes it, and project() never emits it
|
|
66
|
-
// (
|
|
67
|
+
// (Pi's own `oauth` field is an OAuthConfig object, not a boolean).
|
|
67
68
|
oauth?: boolean;
|
|
68
69
|
enabled?: boolean;
|
|
69
70
|
}
|
|
@@ -78,14 +79,10 @@ function isAuth(v: unknown): v is McpAuth {
|
|
|
78
79
|
|
|
79
80
|
// What this entry will actually do when it connects. Explicit `auth` wins; a bearer
|
|
80
81
|
// token implies bearer; a legacy `oauth: false` means no auth; otherwise HTTP servers
|
|
81
|
-
// auto-detect OAuth, which is
|
|
82
|
-
//
|
|
83
|
-
//
|
|
84
|
-
//
|
|
85
|
-
// EXPLICIT auth:"oauth" for a headers-carrying entry, and the adapter checks
|
|
86
|
-
// auth === "oauth" BEFORE its headers check — so we'd force OAuth on exactly the
|
|
87
|
-
// connectors it means to skip it for. Only implicit auth defers to headers; an explicit
|
|
88
|
-
// `auth` from the user still wins.
|
|
82
|
+
// auto-detect OAuth, which is Pi's own behaviour (sign-in is offered when the server
|
|
83
|
+
// answers 401). Only implicit auth defers to custom headers — an entry that carries its
|
|
84
|
+
// credential in a header isn't an OAuth connector — while an explicit `auth` from the
|
|
85
|
+
// user still wins.
|
|
89
86
|
function authOf(e: SourceEntry, transport: McpTransport): McpAuth {
|
|
90
87
|
if (transport !== "http") return "none";
|
|
91
88
|
if (isAuth(e.auth)) return e.auth;
|
|
@@ -139,6 +136,10 @@ export interface McpDraft {
|
|
|
139
136
|
}
|
|
140
137
|
|
|
141
138
|
export interface McpControl {
|
|
139
|
+
// Re-write mcp.json from mcp-desktop.json. Run once at launch by
|
|
140
|
+
// migrateMcpProjection, for a file an older release wrote in pi-mcp-adapter's format.
|
|
141
|
+
// `adopt`: projection entries to take into mcp-desktop.json first when it lacks them.
|
|
142
|
+
reproject(adopt?: Record<string, unknown>): void;
|
|
142
143
|
// Every managed server, non-secret projection. Enabled or not — the app shows
|
|
143
144
|
// disabled connectors so they can be toggled back on.
|
|
144
145
|
list(): RemoteMcpServer[];
|
|
@@ -169,6 +170,139 @@ function cleanArgs(v: unknown): string[] | undefined {
|
|
|
169
170
|
return v.map((x) => String(x ?? "")).filter((s) => s.length > 0);
|
|
170
171
|
}
|
|
171
172
|
|
|
173
|
+
// Pi's server-name rule (core/mcp-servers.js), and an environment variable name.
|
|
174
|
+
const SERVER_NAME = /^[A-Za-z0-9_-]+$/;
|
|
175
|
+
const ENV_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
176
|
+
|
|
177
|
+
function isSseUrl(url: string): boolean {
|
|
178
|
+
try {
|
|
179
|
+
return /\/sse\/?$/i.test(new URL(url).pathname);
|
|
180
|
+
} catch {
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Carry a value written for pi-mcp-adapter into Pi's config-value syntax, meaning
|
|
187
|
+
* exactly what it meant before.
|
|
188
|
+
*
|
|
189
|
+
* Every value here — env, headers, a bearer token — was typed for the adapter, and
|
|
190
|
+
* the app, /connect and the desktop all still write that vocabulary:
|
|
191
|
+
* • `${VAR}`, `$env:VAR` and `{env:VAR}` read the environment; any other `$` is a
|
|
192
|
+
* literal `$`.
|
|
193
|
+
* • A leading `!` runs the rest as a shell command; `!!` is a literal `!`.
|
|
194
|
+
* Pi (core/resolve-config-value.js) reads `${VAR}` and also bare `$VAR`, runs a
|
|
195
|
+
* leading `!`, and escapes with `$$` and `$!`. So the references become `${VAR}`,
|
|
196
|
+
* every other `$` is doubled (a token like `pa$word` would otherwise lose `$word`),
|
|
197
|
+
* and `!!x` becomes `$!x`. A `!command` stays a command, as it was.
|
|
198
|
+
*
|
|
199
|
+
* One difference stays: an unset variable was "" for the adapter, and makes Pi report
|
|
200
|
+
* the server as misconfigured — a louder failure for the same broken config.
|
|
201
|
+
*/
|
|
202
|
+
export function toPiConfigValue(value: string): string {
|
|
203
|
+
// "!!x" is a literal "!" then x, with x's references still read (never a command).
|
|
204
|
+
if (value.startsWith("!!")) return `$!${references(value.slice(2))}`;
|
|
205
|
+
if (value.startsWith("!")) return value;
|
|
206
|
+
return references(value);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// The adapter's three reference forms as Pi's `${VAR}`; every other `$` literal.
|
|
210
|
+
function references(value: string): string {
|
|
211
|
+
const literal = (text: string) => text.replace(/\$/g, "$$$$");
|
|
212
|
+
let out = "";
|
|
213
|
+
let last = 0;
|
|
214
|
+
for (const m of value.matchAll(/\$\{(\w+)\}|\$env:(\w+)|\{env:(\w+)\}/g)) {
|
|
215
|
+
out += literal(value.slice(last, m.index)) + "${" + (m[1] ?? m[2] ?? m[3]) + "}";
|
|
216
|
+
last = m.index! + m[0].length;
|
|
217
|
+
}
|
|
218
|
+
return out + literal(value.slice(last));
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function piConfigValues(values: Record<string, string>): Record<string, string> {
|
|
222
|
+
const out: Record<string, string> = {};
|
|
223
|
+
for (const [k, v] of Object.entries(values)) out[k] = toPiConfigValue(String(v ?? ""));
|
|
224
|
+
return out;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* A bearer token as a whole Authorization header value. A command token (`!cmd`)
|
|
229
|
+
* printed only the token for the adapter, which added "Bearer "; Pi runs a command
|
|
230
|
+
* only as the WHOLE value, so the prefix moves inside it.
|
|
231
|
+
*/
|
|
232
|
+
function bearerHeader(token: string): string {
|
|
233
|
+
if (token.startsWith("!") && !token.startsWith("!!")) return `!printf 'Bearer %s' "$(${token.slice(1)})"`;
|
|
234
|
+
return `Bearer ${toPiConfigValue(token)}`;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* One-time upgrade of a projection written for pi-mcp-adapter. Its marker is the
|
|
239
|
+
* `settings.toolPrefix` pin every earlier release wrote; such a file carries `auth`
|
|
240
|
+
* strings and `bearerToken` fields that Pi rejects, so every HTTP connector in it would
|
|
241
|
+
* vanish. Re-projecting from mcp-desktop.json rewrites it in Pi's format. A file
|
|
242
|
+
* without the marker — Pi's own, or hand-written — is left alone.
|
|
243
|
+
*/
|
|
244
|
+
export function migrateMcpProjection(dir: () => string = agentDir): boolean {
|
|
245
|
+
let raw: any;
|
|
246
|
+
try {
|
|
247
|
+
raw = JSON.parse(readFileSync(join(dir(), "mcp.json"), "utf8"));
|
|
248
|
+
if (raw?.settings?.toolPrefix === undefined) return false;
|
|
249
|
+
} catch {
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
// A server someone added to mcp.json by hand never reached mcp-desktop.json, and
|
|
253
|
+
// re-projecting from that file alone would drop it. Adopt those first.
|
|
254
|
+
makeMcpControl({ dir }).reproject(raw?.mcpServers ?? {});
|
|
255
|
+
return true;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Legacy SSE endpoints of servers that also serve streamable HTTP, which is all Pi
|
|
259
|
+
// speaks. A connector saved with one of these (the catalog shipped them) is moved to
|
|
260
|
+
// the streamable URL wherever it is projected, so it keeps working without a re-save.
|
|
261
|
+
const LEGACY_SSE_URLS: Record<string, string> = {
|
|
262
|
+
"https://mcp.atlassian.com/v1/sse": "https://mcp.atlassian.com/v1/mcp",
|
|
263
|
+
"https://mcp.asana.com/sse": "https://mcp.asana.com/mcp",
|
|
264
|
+
"https://mcp.linear.app/sse": "https://mcp.linear.app/mcp",
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
export function upgradeLegacyUrl(url: string): string {
|
|
268
|
+
return LEGACY_SSE_URLS[url.replace(/\/$/, "")] ?? url;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// One managed entry as a Pi `mcpServers` entry. Exported for the desktop's connector
|
|
272
|
+
// manager (treeview desktop/src/main/mcpService.ts), which writes the same files and
|
|
273
|
+
// must write them the same way. Built field by field rather than
|
|
274
|
+
// spread, because our vocabulary and Pi's differ:
|
|
275
|
+
//
|
|
276
|
+
// • Pi has no bearer field. A static token becomes an `Authorization` header, and a
|
|
277
|
+
// token read from the environment becomes "Bearer ${VAR}". A header named
|
|
278
|
+
// Authorization is also what tells Pi NOT to start OAuth for the server.
|
|
279
|
+
// • Pi's `auth` is `{ provider }` — a Pi /login provider's token — not our
|
|
280
|
+
// "oauth" | "bearer" | "none". Any other value makes Pi reject the whole entry, so
|
|
281
|
+
// ours never reaches the file. OAuth needs no field at all: Pi offers sign-in when
|
|
282
|
+
// a server without an Authorization header answers 401.
|
|
283
|
+
// • Pi and the adapter both expand env and header values, with different syntax:
|
|
284
|
+
// each value is carried over so it means what it meant (toPiConfigValue). Copied
|
|
285
|
+
// verbatim, a token containing "$" would be mangled by Pi's bare-$VAR rule.
|
|
286
|
+
export function toPiServerEntry(e: SourceEntry): Record<string, unknown> {
|
|
287
|
+
const transport: McpTransport = e.transport ?? (e.url ? "http" : "stdio");
|
|
288
|
+
const std: Record<string, unknown> = {};
|
|
289
|
+
if (transport === "stdio") {
|
|
290
|
+
if (e.command) std.command = e.command;
|
|
291
|
+
if (e.args?.length) std.args = e.args;
|
|
292
|
+
if (e.env && Object.keys(e.env).length > 0) std.env = piConfigValues(e.env);
|
|
293
|
+
} else {
|
|
294
|
+
if (e.url) std.url = upgradeLegacyUrl(e.url);
|
|
295
|
+
const headers: Record<string, string> = piConfigValues(e.headers ?? {});
|
|
296
|
+
if (authOf(e, transport) === "bearer") {
|
|
297
|
+
// A stored token wins over the env var name.
|
|
298
|
+
if (e.bearerToken) headers.Authorization = bearerHeader(e.bearerToken);
|
|
299
|
+
else if (e.bearerTokenEnv && ENV_NAME.test(e.bearerTokenEnv)) headers.Authorization = `Bearer \${${e.bearerTokenEnv}}`;
|
|
300
|
+
}
|
|
301
|
+
if (Object.keys(headers).length > 0) std.headers = headers;
|
|
302
|
+
}
|
|
303
|
+
return std;
|
|
304
|
+
}
|
|
305
|
+
|
|
172
306
|
export function makeMcpControl(opts?: {
|
|
173
307
|
// Override the source/projection dir (tests). Defaults to the shared agent dir, so
|
|
174
308
|
// this control and the desktop's mcpService edit the SAME two files.
|
|
@@ -205,59 +339,21 @@ export function makeMcpControl(opts?: {
|
|
|
205
339
|
project(src);
|
|
206
340
|
}
|
|
207
341
|
|
|
208
|
-
// Project the
|
|
209
|
-
//
|
|
210
|
-
//
|
|
342
|
+
// Project the servers into the standard mcp.json Pi reads. An entry with no
|
|
343
|
+
// explicit transport is treated as stdio if it has a command, http if it has a url —
|
|
344
|
+
// matching Pi's own inference.
|
|
211
345
|
//
|
|
212
|
-
//
|
|
213
|
-
//
|
|
214
|
-
//
|
|
215
|
-
//
|
|
216
|
-
// today and stops a future default flip from silently voiding every allow-list.
|
|
346
|
+
// A disabled server is written with Pi's own `enabled: false` rather than left out:
|
|
347
|
+
// Pi keeps it listed (and skips connecting it), and `mcp login` can still find it —
|
|
348
|
+
// a desktop window may activate a connector that is off by default and need to sign
|
|
349
|
+
// in to it.
|
|
217
350
|
function project(src: SourceFile): void {
|
|
218
351
|
const mcpServers: Record<string, unknown> = {};
|
|
219
352
|
for (const [name, e] of Object.entries(src.servers)) {
|
|
220
|
-
|
|
221
|
-
mcpServers[name] = toStandard(e);
|
|
353
|
+
mcpServers[name] = e.enabled === false ? { ...toPiServerEntry(e), enabled: false } : toPiServerEntry(e);
|
|
222
354
|
}
|
|
223
355
|
mkdirSync(dirname(projectionPath()), { recursive: true });
|
|
224
|
-
writeFileAtomic(
|
|
225
|
-
projectionPath(),
|
|
226
|
-
JSON.stringify({ mcpServers, settings: { toolPrefix: "server" } }, null, 2) + "\n",
|
|
227
|
-
);
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
// One managed entry as pi-mcp-adapter's ServerEntry. Built field by field rather
|
|
231
|
-
// than spread, because our vocabulary and the adapter's differ in two places that
|
|
232
|
-
// matter: our `auth: "none"` is its `auth: false`, and our legacy boolean `oauth`
|
|
233
|
-
// collides with its `oauth` (an OAuthConfig OBJECT) and must never reach the file.
|
|
234
|
-
//
|
|
235
|
-
// Getting `auth` right is load-bearing, not cosmetic. The adapter only attaches an
|
|
236
|
-
// Authorization header when `auth === "bearer"` (server-manager.ts), and its
|
|
237
|
-
// supportsOAuth() refuses OAuth outright once custom headers are configured — so an
|
|
238
|
-
// omitted `auth` silently means "no bearer token was ever sent".
|
|
239
|
-
function toStandard(e: SourceEntry): Record<string, unknown> {
|
|
240
|
-
const transport: McpTransport = e.transport ?? (e.url ? "http" : "stdio");
|
|
241
|
-
const std: Record<string, unknown> = {};
|
|
242
|
-
if (transport === "stdio") {
|
|
243
|
-
if (e.command) std.command = e.command;
|
|
244
|
-
if (e.args?.length) std.args = e.args;
|
|
245
|
-
} else {
|
|
246
|
-
if (e.url) std.url = e.url;
|
|
247
|
-
if (e.headers && Object.keys(e.headers).length > 0) std.headers = e.headers;
|
|
248
|
-
const auth = authOf(e, transport);
|
|
249
|
-
if (auth === "bearer") {
|
|
250
|
-
std.auth = "bearer";
|
|
251
|
-
if (e.bearerToken) std.bearerToken = e.bearerToken;
|
|
252
|
-
if (e.bearerTokenEnv) std.bearerTokenEnv = e.bearerTokenEnv;
|
|
253
|
-
} else if (auth === "oauth") {
|
|
254
|
-
std.auth = "oauth";
|
|
255
|
-
} else {
|
|
256
|
-
std.auth = false;
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
if (e.env && Object.keys(e.env).length > 0) std.env = e.env;
|
|
260
|
-
return std;
|
|
356
|
+
writeFileAtomic(projectionPath(), JSON.stringify({ mcpServers }, null, 2) + "\n");
|
|
261
357
|
}
|
|
262
358
|
|
|
263
359
|
function toRemote(name: string, e: SourceEntry): RemoteMcpServer {
|
|
@@ -306,6 +402,18 @@ export function makeMcpControl(opts?: {
|
|
|
306
402
|
}
|
|
307
403
|
|
|
308
404
|
return {
|
|
405
|
+
reproject(adopt?: Record<string, unknown>): void {
|
|
406
|
+
const src = readSource();
|
|
407
|
+
let added = false;
|
|
408
|
+
for (const [name, entry] of Object.entries(adopt ?? {})) {
|
|
409
|
+
if (src.servers[name] || !entry || typeof entry !== "object") continue;
|
|
410
|
+
src.servers[name] = { ...(entry as SourceEntry), enabled: true };
|
|
411
|
+
added = true;
|
|
412
|
+
}
|
|
413
|
+
if (added) writeSource(src);
|
|
414
|
+
else project(src);
|
|
415
|
+
},
|
|
416
|
+
|
|
309
417
|
list(): RemoteMcpServer[] {
|
|
310
418
|
const src = readSource();
|
|
311
419
|
return Object.entries(src.servers).map(([name, e]) => toRemote(name, e));
|
|
@@ -314,6 +422,9 @@ export function makeMcpControl(opts?: {
|
|
|
314
422
|
save(draft: McpDraft): { ok: boolean; message?: string } {
|
|
315
423
|
const name = String(draft?.name ?? "").trim();
|
|
316
424
|
if (!name) return { ok: false, message: "A connector needs a name." };
|
|
425
|
+
// Pi's rule for server names; anything else is skipped as invalid at connect time.
|
|
426
|
+
if (!SERVER_NAME.test(name))
|
|
427
|
+
return { ok: false, message: "A connector name may use only letters, digits, \"-\" and \"_\"." };
|
|
317
428
|
if (draft.transport !== undefined && !isTransport(draft.transport))
|
|
318
429
|
return { ok: false, message: "Unknown transport." };
|
|
319
430
|
if (draft.auth !== undefined && !isAuth(draft.auth))
|
|
@@ -342,6 +453,9 @@ export function makeMcpControl(opts?: {
|
|
|
342
453
|
if (!entry.command) return { ok: false, message: "A local (stdio) connector needs a command." };
|
|
343
454
|
} else {
|
|
344
455
|
if (draft.url !== undefined) entry.url = String(draft.url).trim();
|
|
456
|
+
// A connector saved with a legacy SSE endpoint before Pi: move it on, rather
|
|
457
|
+
// than refuse every later edit of it below.
|
|
458
|
+
if (entry.url) entry.url = upgradeLegacyUrl(entry.url);
|
|
345
459
|
// `auth` is authoritative; the legacy boolean is honoured only when it isn't
|
|
346
460
|
// sent, so an old app build keeps working without being able to override.
|
|
347
461
|
if (draft.auth !== undefined) entry.auth = draft.auth;
|
|
@@ -362,9 +476,9 @@ export function makeMcpControl(opts?: {
|
|
|
362
476
|
else delete entry.headers;
|
|
363
477
|
}
|
|
364
478
|
// A token that arrived without an explicit `auth` means bearer — otherwise the
|
|
365
|
-
//
|
|
366
|
-
//
|
|
367
|
-
//
|
|
479
|
+
// token is stored and never projected into an Authorization header (toPiServerEntry
|
|
480
|
+
// only sends it for bearer), which reads to the user as "my token is saved and
|
|
481
|
+
// the connector still 401s".
|
|
368
482
|
if (draft.auth === undefined && (entry.bearerToken || entry.bearerTokenEnv)) entry.auth = "bearer";
|
|
369
483
|
// Once `auth` is set, the legacy boolean is noise that authOf would have to
|
|
370
484
|
// keep tie-breaking. Drop it.
|
|
@@ -372,6 +486,15 @@ export function makeMcpControl(opts?: {
|
|
|
372
486
|
delete entry.command;
|
|
373
487
|
delete entry.args;
|
|
374
488
|
if (!entry.url) return { ok: false, message: "A remote (http) connector needs a URL." };
|
|
489
|
+
// Pi speaks streamable HTTP only. An SSE endpoint would save and then fail on
|
|
490
|
+
// every connect, so say so now — most servers serve streamable HTTP at /mcp.
|
|
491
|
+
if (isSseUrl(entry.url))
|
|
492
|
+
return {
|
|
493
|
+
ok: false,
|
|
494
|
+
message: "That looks like a legacy SSE endpoint, which isn't supported. Use the server's streamable HTTP URL (often ending in /mcp).",
|
|
495
|
+
};
|
|
496
|
+
if (entry.bearerTokenEnv && !ENV_NAME.test(entry.bearerTokenEnv))
|
|
497
|
+
return { ok: false, message: "The token variable must be an environment variable name, like GITHUB_TOKEN." };
|
|
375
498
|
if (authOf(entry, "http") === "bearer" && !entry.bearerToken && !entry.bearerTokenEnv)
|
|
376
499
|
return { ok: false, message: "A bearer connector needs a token, or the name of an env var holding one." };
|
|
377
500
|
}
|
|
@@ -1303,7 +1303,7 @@ export class RelayClient {
|
|
|
1303
1303
|
// `builtIn` + `managed` describe the MOAT, which is deliberately absent from
|
|
1304
1304
|
// `installed` (it never enters settings "packages"). Without them the app's Browse
|
|
1305
1305
|
// tab — an unfiltered npm `keywords:pi-package` search — offers Add on packages we
|
|
1306
|
-
// already ship: pi-mcp-adapter, pi-subagents and the rpiv packs are on its first
|
|
1306
|
+
// already ship (or retired): pi-mcp-adapter, pi-subagents and the rpiv packs are on its first
|
|
1307
1307
|
// page. Every one of those round-trips to extensionsControl.add() only to come back
|
|
1308
1308
|
// refused ("managed by Privateer"), and until it does the row reads as *not*
|
|
1309
1309
|
// installed, which is the opposite of the truth. So:
|
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
// or a per-server wildcard "<server>__*" — everything else is a builtin allow-list
|
|
5
5
|
// entry.
|
|
6
6
|
//
|
|
7
|
-
// NOTE the selector is NOT the name Pi registers.
|
|
8
|
-
// "<
|
|
9
|
-
//
|
|
10
|
-
//
|
|
7
|
+
// NOTE the selector is NOT the name Pi registers. Pi names an MCP tool
|
|
8
|
+
// "mcp__<server>__<tool>", so a selector must be TRANSLATED before it can grant
|
|
9
|
+
// anything. That translation lives in ../mcp/toolNames.ts; this module only splits and
|
|
10
|
+
// matches.
|
|
11
11
|
|
|
12
12
|
export interface RoutineToolSplit {
|
|
13
13
|
// Builtin tool names (read, glob, ...). Empty → caller falls back to the safe set.
|
package/src/harbor/buildLock.ts
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
// Session CONSTRUCTION is serialized across the whole process. pi-mcp-adapter decides
|
|
2
|
-
// which MCP tools to register directly at extension-activation time, and the only knob
|
|
3
|
-
// for that is the MCP_DIRECT_TOOLS env var — process-global state we set per run to
|
|
4
|
-
// keep each unattended session to its own connector allow-list. Serializing the
|
|
5
|
-
// (short) build window is what stops two concurrent builds from reading each other's
|
|
6
|
-
// value. Prompting is NOT serialized — only the build.
|
|
7
|
-
//
|
|
8
|
-
// Lifted out of harbor/index.ts so the channels runtime can share the SAME lock once
|
|
9
|
-
// it runs inside the harbor: two owners each with their own lock would serialize
|
|
10
|
-
// against themselves and race against each other, which is exactly the bug the lock
|
|
11
|
-
// exists to prevent.
|
|
12
|
-
|
|
13
|
-
let buildLock: Promise<unknown> = Promise.resolve();
|
|
14
|
-
|
|
15
|
-
export function serializeBuild<T>(fn: () => Promise<T>): Promise<T> {
|
|
16
|
-
const run = buildLock.then(fn, fn);
|
|
17
|
-
buildLock = run.catch(() => {});
|
|
18
|
-
return run;
|
|
19
|
-
}
|