@eovidiu/pi-extensions 0.1.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/LICENSE +21 -0
- package/README.md +379 -0
- package/SECURITY.md +13 -0
- package/examples/mcp.json.example +23 -0
- package/examples/project-mcp.override.example.json +17 -0
- package/extensions/mcp-bridge/README.md +51 -0
- package/extensions/mcp-bridge/config-discovery.ts +143 -0
- package/extensions/mcp-bridge/config-sync.ts +242 -0
- package/extensions/mcp-bridge/index.ts +354 -0
- package/extensions/mcp-bridge/logger.ts +35 -0
- package/extensions/mcp-bridge/mcp-client.ts +178 -0
- package/extensions/mcp-bridge/schema-conversion.ts +84 -0
- package/extensions/mcp-bridge/tool-registration.ts +114 -0
- package/extensions/mcp-bridge/types.ts +52 -0
- package/fixtures/claude-code/settings.json +15 -0
- package/fixtures/claude-desktop/claude_desktop_config.json +11 -0
- package/fixtures/codex/config.toml +6 -0
- package/package.json +51 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ovidiu
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
# @eovidiu/pi-extensions
|
|
2
|
+
|
|
3
|
+
Personal [Pi](https://pi.dev) extension package.
|
|
4
|
+
|
|
5
|
+
This repository currently contains one extension: **`mcp-bridge`**, an explicit opt-in MCP compatibility bridge for Pi.
|
|
6
|
+
|
|
7
|
+
> Security note: Pi extensions run as local TypeScript with your user permissions. MCP servers started by this extension also run with your user permissions. Review the source and every MCP server command before enabling it.
|
|
8
|
+
|
|
9
|
+
## Why this exists
|
|
10
|
+
|
|
11
|
+
Pi intentionally does not ship built-in MCP support. The usual Pi approach is to integrate tools directly through skills, CLI wrappers, or extensions.
|
|
12
|
+
|
|
13
|
+
This package is a compatibility bridge for users who already have trusted MCP server configs in other tools and want to reuse them from Pi without giving up Pi's explicit opt-in model.
|
|
14
|
+
|
|
15
|
+
The bridge never starts newly discovered MCP servers automatically. Discovery only records servers as disabled. A server must be explicitly enabled before its process starts or its tools are exposed to Pi.
|
|
16
|
+
|
|
17
|
+
## Features
|
|
18
|
+
|
|
19
|
+
- Discovers MCP server configs from common Claude Desktop, Claude Code, and Codex locations.
|
|
20
|
+
- Syncs discovered servers into a Pi-owned config file: `~/.pi/mcp.json`.
|
|
21
|
+
- Preserves manually added Pi MCP server entries.
|
|
22
|
+
- Preserves existing `enabled` values across resyncs.
|
|
23
|
+
- Defaults newly discovered servers to `enabled: false`.
|
|
24
|
+
- Starts only explicitly enabled MCP stdio servers.
|
|
25
|
+
- Initializes MCP sessions and calls `tools/list`.
|
|
26
|
+
- Converts a conservative MCP JSON Schema subset into Pi/typebox tool schemas.
|
|
27
|
+
- Registers supported MCP tools as Pi tools.
|
|
28
|
+
- Forwards Pi tool calls to MCP `tools/call`.
|
|
29
|
+
- Stops and deactivates tools on disable, restart, session shutdown, and reload.
|
|
30
|
+
- Supports interactive `/mcp-enable` and `/mcp-disable` selectors in Pi's TUI.
|
|
31
|
+
- Supports project-local `.pi/mcp.json` overrides.
|
|
32
|
+
- Supports `allowServers`, `denyServers`, and `maxOutputChars` hardening settings.
|
|
33
|
+
- Writes detailed redacted diagnostics to `~/.pi/mcp-bridge.log` instead of stdout.
|
|
34
|
+
|
|
35
|
+
## Installation
|
|
36
|
+
|
|
37
|
+
### Local development install
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pi install /Users/fameftimie/work/pi-extensions
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
After editing the extension, run this inside Pi:
|
|
44
|
+
|
|
45
|
+
```text
|
|
46
|
+
/reload
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Temporary one-off load
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
pi -e /Users/fameftimie/work/pi-extensions
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Use this for quick testing only. A normal `pi install` is better for ongoing use because the package remains in Pi's resource set.
|
|
56
|
+
|
|
57
|
+
### npm install, after publishing
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
pi install npm:@eovidiu/pi-extensions
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
To pin a published version:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
pi install npm:@eovidiu/pi-extensions@0.1.0
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Quick start
|
|
70
|
+
|
|
71
|
+
1. Install the package.
|
|
72
|
+
2. Start or reload Pi.
|
|
73
|
+
3. Sync detected MCP configs:
|
|
74
|
+
|
|
75
|
+
```text
|
|
76
|
+
/mcp-sync
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
4. Inspect what was found:
|
|
80
|
+
|
|
81
|
+
```text
|
|
82
|
+
/mcp-status
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
5. Enable trusted servers explicitly.
|
|
86
|
+
|
|
87
|
+
Interactive TUI selector:
|
|
88
|
+
|
|
89
|
+
```text
|
|
90
|
+
/mcp-enable
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Direct enable by name:
|
|
94
|
+
|
|
95
|
+
```text
|
|
96
|
+
/mcp-enable claude_desktop__filesystem
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
6. Ask Pi to use one of the registered MCP-backed tools.
|
|
100
|
+
|
|
101
|
+
7. Disable servers when you no longer need them.
|
|
102
|
+
|
|
103
|
+
Interactive TUI selector:
|
|
104
|
+
|
|
105
|
+
```text
|
|
106
|
+
/mcp-disable
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Direct disable by name:
|
|
110
|
+
|
|
111
|
+
```text
|
|
112
|
+
/mcp-disable claude_desktop__filesystem
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Commands
|
|
116
|
+
|
|
117
|
+
| Command | Behavior |
|
|
118
|
+
|---|---|
|
|
119
|
+
| `/mcp-sync` | Rescan known Claude/Codex MCP config locations and update `~/.pi/mcp.json`. Newly discovered servers remain disabled. Enabled servers are started/registered after sync. |
|
|
120
|
+
| `/mcp-status` | Show tracked servers, enabled/disabled state, connection summary, config paths, and log path. |
|
|
121
|
+
| `/mcp-enable` | In interactive Pi sessions, rescan configs and open a selector for disabled detected servers. Selected servers are enabled, started, and registered. |
|
|
122
|
+
| `/mcp-enable <server>` | Enable one server directly, then start/register enabled MCP tools. |
|
|
123
|
+
| `/mcp-disable` | In interactive Pi sessions, open a selector for currently enabled servers. Selected servers are disabled, stopped, and their tools are deactivated. |
|
|
124
|
+
| `/mcp-disable <server>` | Disable one server directly, stop it, and deactivate its tools. |
|
|
125
|
+
| `/mcp-restart` | Restart all enabled MCP servers. |
|
|
126
|
+
| `/mcp-restart <server>` | Restart one MCP server. |
|
|
127
|
+
|
|
128
|
+
Interactive selectors use Pi's `ctx.ui.select()` API. They work in TUI/RPC-capable interactive contexts. In non-UI print/JSON mode, the extension avoids dialogs and reports direct command usage instead.
|
|
129
|
+
|
|
130
|
+
## Config files
|
|
131
|
+
|
|
132
|
+
### Global config
|
|
133
|
+
|
|
134
|
+
The extension owns this file:
|
|
135
|
+
|
|
136
|
+
```text
|
|
137
|
+
~/.pi/mcp.json
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Example:
|
|
141
|
+
|
|
142
|
+
```json
|
|
143
|
+
{
|
|
144
|
+
"version": 1,
|
|
145
|
+
"autoStart": false,
|
|
146
|
+
"servers": {
|
|
147
|
+
"claude_desktop__filesystem": {
|
|
148
|
+
"enabled": false,
|
|
149
|
+
"managedBy": "pi-mcp-bridge",
|
|
150
|
+
"source": "claude-desktop",
|
|
151
|
+
"sourceName": "filesystem",
|
|
152
|
+
"command": "npx",
|
|
153
|
+
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/Users/me/work"],
|
|
154
|
+
"env": {}
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
"maxOutputChars": 20000
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Rules:
|
|
162
|
+
|
|
163
|
+
- Managed discovered entries have `managedBy: "pi-mcp-bridge"`.
|
|
164
|
+
- Managed entries may be updated or removed by `/mcp-sync` when source configs change.
|
|
165
|
+
- Manual entries without that `managedBy` value are preserved.
|
|
166
|
+
- Existing managed `enabled` values are preserved during sync.
|
|
167
|
+
- Newly discovered managed entries are always added with `enabled: false`.
|
|
168
|
+
|
|
169
|
+
### Project override
|
|
170
|
+
|
|
171
|
+
A project can provide an override at:
|
|
172
|
+
|
|
173
|
+
```text
|
|
174
|
+
.pi/mcp.json
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Project entries override global entries with the same server name for the current working directory. Project settings can also provide policy controls:
|
|
178
|
+
|
|
179
|
+
```json
|
|
180
|
+
{
|
|
181
|
+
"version": 1,
|
|
182
|
+
"autoStart": false,
|
|
183
|
+
"servers": {},
|
|
184
|
+
"allowServers": ["claude_desktop__filesystem", "codex__*"],
|
|
185
|
+
"denyServers": ["*production*"],
|
|
186
|
+
"maxOutputChars": 20000
|
|
187
|
+
}
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Use project overrides carefully. Review `.pi/mcp.json` before running Pi in repositories you do not trust.
|
|
191
|
+
|
|
192
|
+
## Discovery sources
|
|
193
|
+
|
|
194
|
+
The extension probes common locations for MCP server definitions.
|
|
195
|
+
|
|
196
|
+
### Claude Desktop
|
|
197
|
+
|
|
198
|
+
```text
|
|
199
|
+
~/Library/Application Support/Claude/claude_desktop_config.json
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### Claude Code
|
|
203
|
+
|
|
204
|
+
```text
|
|
205
|
+
~/.claude.json
|
|
206
|
+
~/.claude/settings.json
|
|
207
|
+
~/.config/claude-code/config.json
|
|
208
|
+
~/.config/claude-code/settings.json
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### Codex
|
|
212
|
+
|
|
213
|
+
```text
|
|
214
|
+
~/.codex/config.toml
|
|
215
|
+
~/.codex/config.json
|
|
216
|
+
~/.config/codex/config.toml
|
|
217
|
+
~/.config/codex/config.json
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Supported config maps include common `mcpServers`, `mcp_servers`, and `mcpServersConfig` shapes. Unsupported or malformed entries are skipped and logged.
|
|
221
|
+
|
|
222
|
+
## Implementation details
|
|
223
|
+
|
|
224
|
+
The extension is implemented as a Pi package with this manifest in `package.json`:
|
|
225
|
+
|
|
226
|
+
```json
|
|
227
|
+
{
|
|
228
|
+
"keywords": ["pi-package", "pi-extension", "mcp"],
|
|
229
|
+
"pi": {
|
|
230
|
+
"extensions": ["./extensions/mcp-bridge"]
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
Main modules:
|
|
236
|
+
|
|
237
|
+
| File | Purpose |
|
|
238
|
+
|---|---|
|
|
239
|
+
| `extensions/mcp-bridge/index.ts` | Pi extension entry point. Registers lifecycle hooks and slash commands. Starts/stops enabled servers and registers tools. |
|
|
240
|
+
| `config-discovery.ts` | Finds and parses known third-party MCP config files. Normalizes discovered server definitions. |
|
|
241
|
+
| `config-sync.ts` | Reads/writes `~/.pi/mcp.json`, merges discovered servers, preserves manual entries, validates names, and performs atomic writes. |
|
|
242
|
+
| `mcp-client.ts` | Starts MCP stdio server processes, initializes MCP clients, lists tools, forwards calls, and stops processes. |
|
|
243
|
+
| `schema-conversion.ts` | Converts supported MCP JSON Schema input schemas to Pi/typebox schemas. |
|
|
244
|
+
| `tool-registration.ts` | Generates Pi tool names, registers MCP-backed tools, truncates large outputs, and deactivates tools when servers stop. |
|
|
245
|
+
| `logger.ts` | Writes redacted diagnostic logs to `~/.pi/mcp-bridge.log`. |
|
|
246
|
+
| `types.ts` | Shared config and discovery types. |
|
|
247
|
+
|
|
248
|
+
### Startup/session lifecycle
|
|
249
|
+
|
|
250
|
+
On `session_start`:
|
|
251
|
+
|
|
252
|
+
1. Discover external MCP configs.
|
|
253
|
+
2. Sync discovered servers into `~/.pi/mcp.json`.
|
|
254
|
+
3. Read the effective config, including optional project overrides.
|
|
255
|
+
4. Stop servers that are no longer enabled.
|
|
256
|
+
5. Start enabled servers only.
|
|
257
|
+
6. Register supported tools from running servers.
|
|
258
|
+
7. Notify a short summary in the TUI.
|
|
259
|
+
|
|
260
|
+
On `session_shutdown`:
|
|
261
|
+
|
|
262
|
+
1. Deactivate all registered MCP-backed tools.
|
|
263
|
+
2. Stop all running MCP server processes.
|
|
264
|
+
|
|
265
|
+
### Enable/disable lifecycle
|
|
266
|
+
|
|
267
|
+
`/mcp-enable` with no argument:
|
|
268
|
+
|
|
269
|
+
1. Runs discovery/sync first so newly detected servers appear.
|
|
270
|
+
2. Reads the global config.
|
|
271
|
+
3. Lists disabled servers.
|
|
272
|
+
4. Opens a selector in interactive Pi sessions.
|
|
273
|
+
5. Enables selected servers in `~/.pi/mcp.json`.
|
|
274
|
+
6. Starts enabled servers and registers tools.
|
|
275
|
+
|
|
276
|
+
`/mcp-disable` with no argument:
|
|
277
|
+
|
|
278
|
+
1. Reads the effective config.
|
|
279
|
+
2. Lists enabled servers.
|
|
280
|
+
3. Opens a selector in interactive Pi sessions.
|
|
281
|
+
4. Disables selected servers in `~/.pi/mcp.json`.
|
|
282
|
+
5. Stops selected servers and deactivates their tools.
|
|
283
|
+
|
|
284
|
+
Direct commands, such as `/mcp-enable <server>` and `/mcp-disable <server>`, skip the selector.
|
|
285
|
+
|
|
286
|
+
### Tool naming
|
|
287
|
+
|
|
288
|
+
Generated Pi tool names are prefixed with `mcp_` and include the normalized source/server/tool identity. Example:
|
|
289
|
+
|
|
290
|
+
```text
|
|
291
|
+
mcp_claude_desktop_filesystem_read_file
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
If a generated name collides, the registrar appends a deterministic short suffix.
|
|
295
|
+
|
|
296
|
+
### Schema conversion
|
|
297
|
+
|
|
298
|
+
The initial schema converter intentionally supports a conservative subset:
|
|
299
|
+
|
|
300
|
+
- object schemas
|
|
301
|
+
- required fields
|
|
302
|
+
- strings
|
|
303
|
+
- numbers
|
|
304
|
+
- integers
|
|
305
|
+
- booleans
|
|
306
|
+
- arrays
|
|
307
|
+
- nested objects
|
|
308
|
+
- simple string enums
|
|
309
|
+
|
|
310
|
+
Unsupported complex schemas are skipped for that specific MCP tool rather than crashing the extension.
|
|
311
|
+
|
|
312
|
+
### Output handling
|
|
313
|
+
|
|
314
|
+
MCP tool outputs can be large. The extension respects `maxOutputChars` and truncates returned tool text so Pi conversations do not balloon unexpectedly.
|
|
315
|
+
|
|
316
|
+
### Logging and secrets
|
|
317
|
+
|
|
318
|
+
The extension does not log to stdout because Pi owns stdout/TUI rendering. Detailed diagnostics go to:
|
|
319
|
+
|
|
320
|
+
```text
|
|
321
|
+
~/.pi/mcp-bridge.log
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
Logs redact common secret-bearing keys such as token, key, secret, password, auth, and credential. Still, avoid storing literal secrets in config files when possible.
|
|
325
|
+
|
|
326
|
+
## Examples and fixtures
|
|
327
|
+
|
|
328
|
+
- `examples/mcp.json.example` — example global Pi MCP config.
|
|
329
|
+
- `examples/project-mcp.override.example.json` — example project override.
|
|
330
|
+
- `fixtures/` — known third-party config shapes used by tests.
|
|
331
|
+
|
|
332
|
+
## Development
|
|
333
|
+
|
|
334
|
+
Install dependencies:
|
|
335
|
+
|
|
336
|
+
```bash
|
|
337
|
+
npm install
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
Run checks:
|
|
341
|
+
|
|
342
|
+
```bash
|
|
343
|
+
npm run typecheck
|
|
344
|
+
npm test
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
Preview npm package contents before publishing:
|
|
348
|
+
|
|
349
|
+
```bash
|
|
350
|
+
npm pack --dry-run
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
## Publishing
|
|
354
|
+
|
|
355
|
+
For a scoped public npm package:
|
|
356
|
+
|
|
357
|
+
```bash
|
|
358
|
+
npm run typecheck
|
|
359
|
+
npm test
|
|
360
|
+
npm pack --dry-run
|
|
361
|
+
npm publish --access public
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
The package includes only the files listed in `package.json`'s `files` field.
|
|
365
|
+
|
|
366
|
+
## Security checklist before enabling a server
|
|
367
|
+
|
|
368
|
+
1. Run `/mcp-status` and inspect the server name, source, and command.
|
|
369
|
+
2. Open `~/.pi/mcp.json` and review `command`, `args`, and `env`.
|
|
370
|
+
3. Prefer environment variable references over literal secret values.
|
|
371
|
+
4. Use `allowServers` and `denyServers` for project-level hardening.
|
|
372
|
+
5. Review project-local `.pi/mcp.json` files before using Pi in untrusted repositories.
|
|
373
|
+
6. Disable servers you are not actively using.
|
|
374
|
+
|
|
375
|
+
See `SECURITY.md` for the short-form security policy.
|
|
376
|
+
|
|
377
|
+
## License
|
|
378
|
+
|
|
379
|
+
MIT. See `LICENSE`.
|
package/SECURITY.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Security
|
|
2
|
+
|
|
3
|
+
Pi extensions execute with your user permissions. MCP servers started by this package also execute with your user permissions.
|
|
4
|
+
|
|
5
|
+
Before enabling an MCP server:
|
|
6
|
+
|
|
7
|
+
1. Review the command and arguments in `~/.pi/mcp.json`.
|
|
8
|
+
2. Prefer environment-variable references over literal secrets.
|
|
9
|
+
3. Use `enabled: false` until you intentionally want to start a server.
|
|
10
|
+
4. Use `allowServers` / `denyServers` to restrict startup when needed.
|
|
11
|
+
5. Review project-local `.pi/mcp.json` files before running Pi in untrusted repositories.
|
|
12
|
+
|
|
13
|
+
This extension redacts common secret key names in logs, but you should still avoid storing literal secrets in config files.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"autoStart": false,
|
|
4
|
+
"servers": {
|
|
5
|
+
"claude_desktop__filesystem": {
|
|
6
|
+
"enabled": false,
|
|
7
|
+
"managedBy": "pi-mcp-bridge",
|
|
8
|
+
"source": "claude-desktop",
|
|
9
|
+
"sourceName": "filesystem",
|
|
10
|
+
"command": "npx",
|
|
11
|
+
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/Users/fameftimie/work"],
|
|
12
|
+
"env": {}
|
|
13
|
+
},
|
|
14
|
+
"manual__example": {
|
|
15
|
+
"enabled": false,
|
|
16
|
+
"source": "manual",
|
|
17
|
+
"sourceName": "example",
|
|
18
|
+
"command": "example-mcp-server",
|
|
19
|
+
"args": [],
|
|
20
|
+
"env": {}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"autoStart": false,
|
|
4
|
+
"maxOutputChars": 20000,
|
|
5
|
+
"allowServers": ["claude_desktop__filesystem", "codex__playwright"],
|
|
6
|
+
"denyServers": ["*production*"],
|
|
7
|
+
"servers": {
|
|
8
|
+
"manual__project_local": {
|
|
9
|
+
"enabled": false,
|
|
10
|
+
"source": "manual",
|
|
11
|
+
"sourceName": "project-local",
|
|
12
|
+
"command": "example-mcp-server",
|
|
13
|
+
"args": [],
|
|
14
|
+
"env": {}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# mcp-bridge
|
|
2
|
+
|
|
3
|
+
Explicit opt-in MCP compatibility bridge for Pi.
|
|
4
|
+
|
|
5
|
+
Current implementation covers Phase 1 through Phase 4:
|
|
6
|
+
|
|
7
|
+
- discovers MCP config candidates from Claude Desktop, Claude Code, and Codex
|
|
8
|
+
- syncs supported server entries into `~/.pi/mcp.json`
|
|
9
|
+
- preserves manually added Pi MCP server entries
|
|
10
|
+
- preserves `enabled` values for previously discovered managed entries
|
|
11
|
+
- defaults newly discovered servers to `enabled: false`
|
|
12
|
+
- writes a redacted debug log to `~/.pi/mcp-bridge.log`
|
|
13
|
+
- provides explicit enable/disable controls with server-name validation and completions
|
|
14
|
+
- serializes in-process config mutations to avoid command-handler races
|
|
15
|
+
- starts enabled MCP stdio servers only
|
|
16
|
+
- initializes MCP client sessions and lists tools
|
|
17
|
+
- converts a conservative JSON Schema subset to Pi/typebox tool schemas
|
|
18
|
+
- registers supported MCP tools as Pi tools
|
|
19
|
+
- forwards Pi tool calls to MCP `tools/call`
|
|
20
|
+
- stops/deactivates tools on disable, restart, and session shutdown
|
|
21
|
+
- supports project-local `.pi/mcp.json` overrides
|
|
22
|
+
- supports `allowServers`, `denyServers`, and `maxOutputChars`
|
|
23
|
+
- includes fixtures/tests for hardening
|
|
24
|
+
|
|
25
|
+
## Commands
|
|
26
|
+
|
|
27
|
+
```text
|
|
28
|
+
/mcp-sync
|
|
29
|
+
/mcp-status
|
|
30
|
+
/mcp-enable [server]
|
|
31
|
+
/mcp-disable [server]
|
|
32
|
+
/mcp-restart
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
`/mcp-enable <server>` updates `~/.pi/mcp.json` and immediately attempts to start that enabled server. In interactive Pi sessions, `/mcp-enable` with no argument rescans detected MCP configs, opens a small selector for disabled servers, and enables the selected servers. `/mcp-disable <server>` stops the server and deactivates its Pi tools. In interactive Pi sessions, `/mcp-disable` with no argument opens a small selector for enabled servers and disables the selected servers. `/mcp-restart` restarts one enabled server or all enabled servers.
|
|
36
|
+
|
|
37
|
+
## Config hardening
|
|
38
|
+
|
|
39
|
+
Optional settings in `~/.pi/mcp.json` or project-local `.pi/mcp.json`:
|
|
40
|
+
|
|
41
|
+
```json
|
|
42
|
+
{
|
|
43
|
+
"allowServers": ["claude_desktop__filesystem", "codex__*"],
|
|
44
|
+
"denyServers": ["*production*"],
|
|
45
|
+
"maxOutputChars": 20000
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Safety invariant
|
|
50
|
+
|
|
51
|
+
Installing or reloading this extension must never execute newly discovered MCP server commands. Discovered servers are synced as disabled by default and must be explicitly enabled before future bridge phases may start them.
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { access, readFile } from "node:fs/promises";
|
|
2
|
+
import { constants } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import TOML from "@iarna/toml";
|
|
6
|
+
import { MANAGED_BY, type DiscoveredMcpServer, type DiscoveryEvent, type DiscoveryResult, type McpSource } from "./types.js";
|
|
7
|
+
|
|
8
|
+
interface Candidate {
|
|
9
|
+
source: McpSource;
|
|
10
|
+
path: string;
|
|
11
|
+
format: "json" | "toml";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const CANDIDATES: Candidate[] = [
|
|
15
|
+
{ source: "claude-desktop", path: "~/Library/Application Support/Claude/claude_desktop_config.json", format: "json" },
|
|
16
|
+
{ source: "claude-code", path: "~/.claude.json", format: "json" },
|
|
17
|
+
{ source: "claude-code", path: "~/.claude/settings.json", format: "json" },
|
|
18
|
+
{ source: "claude-code", path: "~/.config/claude-code/config.json", format: "json" },
|
|
19
|
+
{ source: "claude-code", path: "~/.config/claude-code/settings.json", format: "json" },
|
|
20
|
+
{ source: "codex", path: "~/.codex/config.toml", format: "toml" },
|
|
21
|
+
{ source: "codex", path: "~/.codex/config.json", format: "json" },
|
|
22
|
+
{ source: "codex", path: "~/.config/codex/config.toml", format: "toml" },
|
|
23
|
+
{ source: "codex", path: "~/.config/codex/config.json", format: "json" },
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
export function expandHome(path: string): string {
|
|
27
|
+
return path === "~" ? homedir() : path.startsWith("~/") ? join(homedir(), path.slice(2)) : path;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function discoverMcpServers(): Promise<DiscoveryResult> {
|
|
31
|
+
const servers: Record<string, DiscoveredMcpServer> = {};
|
|
32
|
+
const events: DiscoveryEvent[] = [];
|
|
33
|
+
|
|
34
|
+
for (const candidate of CANDIDATES) {
|
|
35
|
+
const path = expandHome(candidate.path);
|
|
36
|
+
try {
|
|
37
|
+
await access(path, constants.R_OK);
|
|
38
|
+
events.push({ path, source: candidate.source, status: "found" });
|
|
39
|
+
} catch {
|
|
40
|
+
events.push({ path, source: candidate.source, status: "missing" });
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
let parsed: unknown;
|
|
45
|
+
try {
|
|
46
|
+
const text = await readFile(path, "utf8");
|
|
47
|
+
parsed = candidate.format === "toml" ? TOML.parse(text) : JSON.parse(text);
|
|
48
|
+
events.push({ path, source: candidate.source, status: "parsed" });
|
|
49
|
+
} catch (error) {
|
|
50
|
+
events.push({ path, source: candidate.source, status: "parse-error", message: errorMessage(error) });
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const maps = findMcpServerMaps(parsed);
|
|
55
|
+
if (maps.length === 0) {
|
|
56
|
+
events.push({ path, source: candidate.source, status: "unsupported", message: "No mcpServers or mcp_servers map found" });
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
let accepted = 0;
|
|
61
|
+
for (const map of maps) {
|
|
62
|
+
for (const [sourceName, rawServer] of Object.entries(map)) {
|
|
63
|
+
const normalized = normalizeServer(candidate.source, sourceName, rawServer, path);
|
|
64
|
+
if (!normalized) continue;
|
|
65
|
+
servers[makeServerKey(candidate.source, sourceName)] = normalized;
|
|
66
|
+
accepted++;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
events.push({
|
|
71
|
+
path,
|
|
72
|
+
source: candidate.source,
|
|
73
|
+
status: accepted > 0 ? "contains-mcp" : "unsupported",
|
|
74
|
+
message: accepted > 0 ? `${accepted} MCP server(s)` : "MCP map found, but no supported server entries",
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return { servers, events };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function findMcpServerMaps(value: unknown, depth = 0): Array<Record<string, unknown>> {
|
|
82
|
+
if (!value || typeof value !== "object" || Array.isArray(value) || depth > 5) return [];
|
|
83
|
+
const obj = value as Record<string, unknown>;
|
|
84
|
+
const maps: Array<Record<string, unknown>> = [];
|
|
85
|
+
|
|
86
|
+
for (const key of ["mcpServers", "mcp_servers", "mcpServersConfig"]) {
|
|
87
|
+
const maybe = obj[key];
|
|
88
|
+
if (maybe && typeof maybe === "object" && !Array.isArray(maybe)) {
|
|
89
|
+
maps.push(maybe as Record<string, unknown>);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
for (const child of Object.values(obj)) {
|
|
94
|
+
if (child && typeof child === "object") {
|
|
95
|
+
maps.push(...findMcpServerMaps(child, depth + 1));
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return maps;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function normalizeServer(source: McpSource, sourceName: string, raw: unknown, configPath: string): DiscoveredMcpServer | null {
|
|
103
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
104
|
+
const obj = raw as Record<string, unknown>;
|
|
105
|
+
const command = typeof obj.command === "string" ? obj.command : undefined;
|
|
106
|
+
if (!command) return null;
|
|
107
|
+
|
|
108
|
+
const args = Array.isArray(obj.args) ? obj.args.filter((arg): arg is string => typeof arg === "string") : [];
|
|
109
|
+
const env = normalizeEnv(obj.env);
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
enabled: false,
|
|
113
|
+
managedBy: MANAGED_BY,
|
|
114
|
+
source,
|
|
115
|
+
sourceName,
|
|
116
|
+
command,
|
|
117
|
+
args,
|
|
118
|
+
env,
|
|
119
|
+
configPath,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function normalizeEnv(raw: unknown): Record<string, string> {
|
|
124
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return {};
|
|
125
|
+
const out: Record<string, string> = {};
|
|
126
|
+
for (const [key, value] of Object.entries(raw as Record<string, unknown>)) {
|
|
127
|
+
if (typeof value === "string") out[key] = value;
|
|
128
|
+
else if (typeof value === "number" || typeof value === "boolean") out[key] = String(value);
|
|
129
|
+
}
|
|
130
|
+
return out;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function makeServerKey(source: McpSource, sourceName: string): string {
|
|
134
|
+
return `${normalizeName(source)}__${normalizeName(sourceName)}`;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function normalizeName(value: string): string {
|
|
138
|
+
return value.replace(/[^A-Za-z0-9_]+/g, "_").replace(/_+/g, "_").replace(/^_|_$/g, "").toLowerCase() || "server";
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function errorMessage(error: unknown): string {
|
|
142
|
+
return error instanceof Error ? error.message : String(error);
|
|
143
|
+
}
|