@ikuma.cloud/pix-mcp 0.0.9 → 0.0.10
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 +44 -24
- package/package.json +1 -1
- package/src/config.ts +50 -5
- package/src/index.ts +61 -12
package/README.md
CHANGED
|
@@ -28,28 +28,45 @@ Review the file first; `npx` downloads and runs the pinned package.
|
|
|
28
28
|
|
|
29
29
|
## Configuration
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
search, global config merge, automatic import, or persistent metadata cache.
|
|
33
|
-
Relative `--mcp-config` paths resolve from that working directory; a stdio
|
|
34
|
-
server's `cwd` resolves from the config directory and defaults to that directory.
|
|
31
|
+
Without an explicit selection, the adapter reads these files in order:
|
|
35
32
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
33
|
+
1. `<agent-dir>/mcp.json` for user-level servers. Pi's agent directory defaults
|
|
34
|
+
to `~/.pi/agent` and can be changed with `PI_CODING_AGENT_DIR`.
|
|
35
|
+
2. `.mcp.json` in Pi's working directory for project-level servers.
|
|
39
36
|
|
|
40
|
-
|
|
41
|
-
|
|
37
|
+
Missing files are ignored. Project declarations replace global declarations by
|
|
38
|
+
server name as complete entries; fields are not merged. A project entry with
|
|
39
|
+
`"disabled": true` masks the corresponding global server. An invalid project
|
|
40
|
+
entry also masks its global counterpart and reports the validation issue instead
|
|
41
|
+
of starting the global definition. Relative stdio `cwd` values resolve from the
|
|
42
|
+
file that declares the server and default to that file's directory. There is no
|
|
43
|
+
ancestor search, cross-client import, or persistent metadata cache.
|
|
44
|
+
|
|
45
|
+
Use `--mcp-config <path>` to read only that file for the session, without global
|
|
46
|
+
or project fallback. A relative explicit path resolves from Pi's working
|
|
47
|
+
directory. The adapter automatically starts every effective enabled server in all
|
|
48
|
+
modes without a confirmation prompt.
|
|
42
49
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
to `true` to prevent it from starting.
|
|
50
|
+
`getAgentDir()` cannot observe an `agentDir` supplied only through Pi 0.87's SDK
|
|
51
|
+
because extensions do not receive it. Embedded users should also set
|
|
52
|
+
`PI_CODING_AGENT_DIR` when they want the global file to follow an SDK override.
|
|
47
53
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
54
|
+
**Breaking change:** `--mcp-trust-config` has been removed. Remove it from existing
|
|
55
|
+
launch commands; print and JSON sessions also load the default files
|
|
56
|
+
automatically.
|
|
57
|
+
|
|
58
|
+
Review both files before starting Pi: they can launch arbitrary local programs
|
|
59
|
+
and contact remote services. Global servers start in every working directory. A
|
|
60
|
+
bare project `.mcp.json` is not protected by Pi's project-trust mechanism, and
|
|
61
|
+
the adapter is not an OS sandbox. Set a server's `disabled` field to `true` to
|
|
62
|
+
prevent it from starting.
|
|
63
|
+
|
|
64
|
+
At session startup, interactive and RPC sessions display every successfully
|
|
65
|
+
loaded file as `MCP config (<scope>): <absolute-path>`, in precedence order. They
|
|
66
|
+
display `MCP config: none found` when no applicable file exists, or a path-only
|
|
67
|
+
error notice when a file fails to load. These notices occur before server startup
|
|
68
|
+
and never include configuration contents or indicate that every server connected.
|
|
69
|
+
Print and JSON sessions remain silent.
|
|
53
70
|
|
|
54
71
|
```json
|
|
55
72
|
{
|
|
@@ -110,15 +127,17 @@ Unknown fields invalidate their server entry rather than being silently ignored.
|
|
|
110
127
|
|
|
111
128
|
### Configuration errors
|
|
112
129
|
|
|
113
|
-
An invalid server entry is skipped without hiding healthy servers.
|
|
114
|
-
`servers` list includes a safe diagnostic for each skipped entry; a
|
|
115
|
-
name can still be used with `mcp({ action: "list", server: "name" })` to inspect
|
|
130
|
+
An invalid server entry is skipped without hiding unrelated healthy servers.
|
|
131
|
+
Discovery's `servers` list includes a safe diagnostic for each skipped entry; a
|
|
132
|
+
valid server name can still be used with `mcp({ action: "list", server: "name" })` to inspect
|
|
116
133
|
its status. Invalid names are replaced with their one-based entry positions.
|
|
117
134
|
Diagnostics identify supported fields or migration steps without echoing URLs,
|
|
118
135
|
commands, headers, argument values, or environment-variable names.
|
|
119
136
|
|
|
120
137
|
Malformed JSON, invalid root structure, unknown root fields, and the file/server
|
|
121
|
-
count limits remain fatal
|
|
138
|
+
count limits remain fatal. A fatal error in either default file prevents servers
|
|
139
|
+
from the other file from starting. Each file may declare at most 32 entries, and
|
|
140
|
+
the merged result may enable at most 32 servers. An invalid-only configuration
|
|
122
141
|
reports that no valid servers remain. Disabled entries are omitted, not reported
|
|
123
142
|
as failed connections. Correct the file and reload Pi to retry.
|
|
124
143
|
|
|
@@ -384,8 +403,9 @@ These artifacts may contain sensitive data and are **not deleted at session
|
|
|
384
403
|
shutdown**; remove them when no
|
|
385
404
|
longer needed. Output limits are not a complete memory or security sandbox.
|
|
386
405
|
|
|
387
|
-
|
|
388
|
-
|
|
406
|
+
Each configuration file is limited to 256 KiB and 32 entries; the merged result
|
|
407
|
+
can enable at most 32 servers. Startup connects at most four servers concurrently.
|
|
408
|
+
Each tool or prompt catalog is limited to 1000 entries,
|
|
389
409
|
100 pagination cursors, and 2 MiB of metadata. Direct resources and templates
|
|
390
410
|
share a 1000-entry and 2 MiB limit, with up to 100 cursors for each endpoint.
|
|
391
411
|
Individual input/output schemas are limited to 64 KiB. Prompt arguments and
|
package/package.json
CHANGED
package/src/config.ts
CHANGED
|
@@ -31,6 +31,19 @@ export interface Config {
|
|
|
31
31
|
servers: ServerConfig[];
|
|
32
32
|
issues: ConfigIssue[];
|
|
33
33
|
}
|
|
34
|
+
export interface MergedConfig {
|
|
35
|
+
servers: ServerConfig[];
|
|
36
|
+
issues: ConfigIssue[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
type ConfigDeclaration = ServerConfig | ConfigIssue | undefined;
|
|
40
|
+
// Raw names can contain credentials. Keep precedence identities private while
|
|
41
|
+
// exposing only validated server names or positional issue labels.
|
|
42
|
+
const declarationsByConfig = new WeakMap<
|
|
43
|
+
Config,
|
|
44
|
+
Map<string, ConfigDeclaration>
|
|
45
|
+
>();
|
|
46
|
+
const SERVER_NAME_PATTERN = /^[A-Za-z0-9_-]{1,48}$/;
|
|
34
47
|
|
|
35
48
|
class ConfigError extends Error {
|
|
36
49
|
readonly field: string | undefined;
|
|
@@ -121,7 +134,7 @@ function parseServer(
|
|
|
121
134
|
path: string,
|
|
122
135
|
env: NodeJS.ProcessEnv,
|
|
123
136
|
): ServerConfig | undefined {
|
|
124
|
-
if (
|
|
137
|
+
if (!SERVER_NAME_PATTERN.test(name))
|
|
125
138
|
invalid(
|
|
126
139
|
undefined,
|
|
127
140
|
"Server names must contain 1–48 ASCII letters, digits, underscores, or hyphens.",
|
|
@@ -268,15 +281,17 @@ export async function readConfig(
|
|
|
268
281
|
invalid("mcpServers", "At most 32 servers are supported.");
|
|
269
282
|
const servers: ServerConfig[] = [];
|
|
270
283
|
const issues: ConfigIssue[] = [];
|
|
284
|
+
const declarations = new Map<string, ConfigDeclaration>();
|
|
271
285
|
for (const [index, [name, raw]] of entries.entries()) {
|
|
272
286
|
try {
|
|
273
287
|
const server = parseServer(name, raw, path, env);
|
|
288
|
+
declarations.set(name, server);
|
|
274
289
|
if (server) servers.push(server);
|
|
275
290
|
} catch (error) {
|
|
276
291
|
// Invalid names and unknown keys can themselves contain credentials. Only
|
|
277
292
|
// validated names and our fixed validation messages reach discovery.
|
|
278
|
-
|
|
279
|
-
name:
|
|
293
|
+
const issue: ConfigIssue = {
|
|
294
|
+
name: SERVER_NAME_PATTERN.test(name)
|
|
280
295
|
? name
|
|
281
296
|
: `Invalid server #${index + 1}`,
|
|
282
297
|
...(error instanceof ConfigError && error.field
|
|
@@ -286,8 +301,38 @@ export async function readConfig(
|
|
|
286
301
|
error instanceof ConfigError
|
|
287
302
|
? error.message
|
|
288
303
|
: "Invalid MCP configuration. Server could not be validated.",
|
|
289
|
-
}
|
|
304
|
+
};
|
|
305
|
+
declarations.set(name, issue);
|
|
306
|
+
issues.push(issue);
|
|
290
307
|
}
|
|
291
308
|
}
|
|
292
|
-
|
|
309
|
+
const config = { path, servers, issues };
|
|
310
|
+
declarationsByConfig.set(config, declarations);
|
|
311
|
+
return config;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
export function mergeConfigs(configs: Config[]): MergedConfig {
|
|
315
|
+
const declarations = new Map<string, ConfigDeclaration>();
|
|
316
|
+
for (const config of configs) {
|
|
317
|
+
const sourceDeclarations = declarationsByConfig.get(config);
|
|
318
|
+
if (!sourceDeclarations)
|
|
319
|
+
invalid(undefined, "Configuration declarations are unavailable.");
|
|
320
|
+
for (const [name, declaration] of sourceDeclarations) {
|
|
321
|
+
// Reinsertion makes effective declaration order follow source precedence.
|
|
322
|
+
declarations.delete(name);
|
|
323
|
+
declarations.set(name, declaration);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
const merged: MergedConfig = { servers: [], issues: [] };
|
|
327
|
+
for (const declaration of declarations.values()) {
|
|
328
|
+
if (!declaration) continue;
|
|
329
|
+
if ("message" in declaration) merged.issues.push(declaration);
|
|
330
|
+
else merged.servers.push(declaration);
|
|
331
|
+
}
|
|
332
|
+
if (merged.servers.length > 32)
|
|
333
|
+
invalid(
|
|
334
|
+
"mcpServers",
|
|
335
|
+
"At most 32 enabled servers are supported across merged configurations.",
|
|
336
|
+
);
|
|
337
|
+
return merged;
|
|
293
338
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { resolve } from "node:path";
|
|
2
2
|
import { Type } from "@earendil-works/pi-ai";
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
import {
|
|
4
|
+
getAgentDir,
|
|
5
|
+
type ExtensionAPI,
|
|
6
|
+
type ExtensionCommandContext,
|
|
7
|
+
type ExtensionContext,
|
|
7
8
|
} from "@earendil-works/pi-coding-agent";
|
|
8
9
|
import type {
|
|
9
10
|
Prompt,
|
|
@@ -13,7 +14,12 @@ import type {
|
|
|
13
14
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
14
15
|
import { compact, entry, search, summary, type Entry } from "./catalog.ts";
|
|
15
16
|
import { Connection } from "./client.ts";
|
|
16
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
mergeConfigs,
|
|
19
|
+
readConfig,
|
|
20
|
+
type ConfigIssue,
|
|
21
|
+
type ServerConfig,
|
|
22
|
+
} from "./config.ts";
|
|
17
23
|
import {
|
|
18
24
|
formatEditableContent,
|
|
19
25
|
formatResult,
|
|
@@ -69,6 +75,12 @@ interface ResourceSelection {
|
|
|
69
75
|
item: ResourceEntry;
|
|
70
76
|
}
|
|
71
77
|
|
|
78
|
+
type ConfigScope = "explicit" | "global" | "project";
|
|
79
|
+
interface ConfigSource {
|
|
80
|
+
scope: ConfigScope;
|
|
81
|
+
path: string;
|
|
82
|
+
}
|
|
83
|
+
|
|
72
84
|
interface State {
|
|
73
85
|
alive: boolean;
|
|
74
86
|
status: string;
|
|
@@ -111,7 +123,7 @@ export default function mcp(pi: ExtensionAPI) {
|
|
|
111
123
|
pi.registerFlag("mcp-config", {
|
|
112
124
|
type: "string",
|
|
113
125
|
description:
|
|
114
|
-
"Read this MCP config file for this session (
|
|
126
|
+
"Read only this MCP config file for this session (otherwise merge the global and project configs).",
|
|
115
127
|
});
|
|
116
128
|
pi.registerCommand("mcp-prompt", {
|
|
117
129
|
description: "Select, list, or run a user-controlled MCP prompt",
|
|
@@ -810,6 +822,8 @@ export default function mcp(pi: ExtensionAPI) {
|
|
|
810
822
|
|
|
811
823
|
async function start(ctx: ExtensionContext) {
|
|
812
824
|
await stop();
|
|
825
|
+
const showConfigNotices =
|
|
826
|
+
ctx.hasUI && (ctx.mode === "tui" || ctx.mode === "rpc");
|
|
813
827
|
const owner: State = {
|
|
814
828
|
alive: true,
|
|
815
829
|
status: "No MCP servers configured.",
|
|
@@ -824,19 +838,51 @@ export default function mcp(pi: ExtensionAPI) {
|
|
|
824
838
|
draftGuards: new Set(),
|
|
825
839
|
};
|
|
826
840
|
state = owner;
|
|
841
|
+
let failureNotice: string | undefined;
|
|
827
842
|
try {
|
|
828
843
|
const flag = pi.getFlag("mcp-config");
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
844
|
+
if (flag === "") {
|
|
845
|
+
failureNotice = "MCP config (explicit) failed: no path provided";
|
|
846
|
+
throw new Error(
|
|
847
|
+
"The explicitly selected MCP configuration path is empty.",
|
|
848
|
+
);
|
|
849
|
+
}
|
|
850
|
+
const explicit = typeof flag === "string";
|
|
851
|
+
const sources: ConfigSource[] = explicit
|
|
852
|
+
? [{ scope: "explicit", path: resolve(ctx.cwd, flag) }]
|
|
853
|
+
: [
|
|
854
|
+
{
|
|
855
|
+
scope: "global",
|
|
856
|
+
path: resolve(getAgentDir(), "mcp.json"),
|
|
857
|
+
},
|
|
858
|
+
{ scope: "project", path: resolve(ctx.cwd, ".mcp.json") },
|
|
859
|
+
];
|
|
860
|
+
const loaded = [];
|
|
861
|
+
for (const source of sources) {
|
|
862
|
+
failureNotice = `MCP config (${source.scope}) failed: ${source.path}`;
|
|
863
|
+
const config = await readConfig(source.path);
|
|
864
|
+
current(owner);
|
|
865
|
+
failureNotice = undefined;
|
|
866
|
+
if (config) {
|
|
867
|
+
loaded.push({ ...source, config });
|
|
868
|
+
if (showConfigNotices)
|
|
869
|
+
ctx.ui.notify(
|
|
870
|
+
`MCP config (${source.scope}): ${source.path}`,
|
|
871
|
+
"info",
|
|
872
|
+
);
|
|
873
|
+
}
|
|
874
|
+
}
|
|
832
875
|
current(owner);
|
|
833
|
-
if (
|
|
876
|
+
if (loaded.length === 0) {
|
|
834
877
|
if (explicit)
|
|
835
878
|
owner.status =
|
|
836
879
|
"The explicitly selected MCP configuration does not exist.";
|
|
880
|
+
if (showConfigNotices) ctx.ui.notify("MCP config: none found", "info");
|
|
837
881
|
return;
|
|
838
882
|
}
|
|
839
|
-
|
|
883
|
+
failureNotice = "MCP configuration merge failed.";
|
|
884
|
+
const config = mergeConfigs(loaded.map((source) => source.config));
|
|
885
|
+
failureNotice = undefined;
|
|
840
886
|
owner.configIssues = config.issues;
|
|
841
887
|
owner.status =
|
|
842
888
|
config.issues.length > 0
|
|
@@ -870,9 +916,12 @@ export default function mcp(pi: ExtensionAPI) {
|
|
|
870
916
|
}),
|
|
871
917
|
);
|
|
872
918
|
} catch (error) {
|
|
873
|
-
if (owner.alive)
|
|
919
|
+
if (owner.alive) {
|
|
874
920
|
owner.status =
|
|
875
921
|
error instanceof Error ? error.message : "MCP initialization failed.";
|
|
922
|
+
if (showConfigNotices)
|
|
923
|
+
ctx.ui.notify(failureNotice ?? "MCP initialization failed.", "error");
|
|
924
|
+
}
|
|
876
925
|
} finally {
|
|
877
926
|
if (owner.alive && state === owner) registerDiscovery();
|
|
878
927
|
}
|