pi-mcp-adapter 1.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/ARCHITECTURE.md +572 -0
- package/CHANGELOG.md +48 -0
- package/LICENSE +21 -0
- package/README.md +189 -0
- package/config.ts +132 -0
- package/index.ts +907 -0
- package/lifecycle.ts +59 -0
- package/oauth-handler.ts +57 -0
- package/package.json +51 -0
- package/resource-tools.ts +45 -0
- package/server-manager.ts +242 -0
- package/tool-registrar.ts +77 -0
- package/types.ts +112 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Nico Bailon
|
|
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,189 @@
|
|
|
1
|
+
# Pi MCP Adapter
|
|
2
|
+
|
|
3
|
+
Connect [Pi](https://github.com/badlogic/pi-mono/) to any MCP server without burning your context window.
|
|
4
|
+
|
|
5
|
+
```json
|
|
6
|
+
{
|
|
7
|
+
"mcpServers": {
|
|
8
|
+
"chrome-devtools": {
|
|
9
|
+
"command": "npx",
|
|
10
|
+
"args": ["-y", "chrome-devtools-mcp@latest"]
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## The Problem
|
|
17
|
+
|
|
18
|
+
MCP servers expose tools for databases, browsers, file systems, APIs. But each tool definition costs ~200 tokens when sent to the LLM. A server with 50 tools? That's 10,000 tokens gone before you've even started. Three servers and you've lost 30K tokens to tool definitions alone.
|
|
19
|
+
|
|
20
|
+
## The Solution
|
|
21
|
+
|
|
22
|
+
One proxy tool. The LLM searches for what it needs, sees the schema, and calls it:
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
mcp({ search: "screenshot" })
|
|
26
|
+
```
|
|
27
|
+
```
|
|
28
|
+
chrome_devtools_take_screenshot
|
|
29
|
+
Take a screenshot of the page or element.
|
|
30
|
+
|
|
31
|
+
Parameters:
|
|
32
|
+
format (enum: "png", "jpeg", "webp") [default: "png"]
|
|
33
|
+
fullPage (boolean) - Full page instead of viewport
|
|
34
|
+
```
|
|
35
|
+
```
|
|
36
|
+
mcp({ tool: "chrome_devtools_take_screenshot", args: { format: "png" } })
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Two calls. ~200 tokens for the proxy tool instead of 30K+ for every tool definition.
|
|
40
|
+
|
|
41
|
+
## Install
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
cd ~/.pi/agent/extensions
|
|
45
|
+
npm install pi-mcp-adapter
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Or clone it:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
git clone https://github.com/nicobailon/pi-mcp-adapter
|
|
52
|
+
cd pi-mcp-adapter && npm install
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Restart Pi.
|
|
56
|
+
|
|
57
|
+
## Config
|
|
58
|
+
|
|
59
|
+
Create `~/.pi/agent/mcp.json`:
|
|
60
|
+
|
|
61
|
+
```json
|
|
62
|
+
{
|
|
63
|
+
"mcpServers": {
|
|
64
|
+
"chrome-devtools": {
|
|
65
|
+
"command": "npx",
|
|
66
|
+
"args": ["-y", "chrome-devtools-mcp@latest"],
|
|
67
|
+
"lifecycle": "keep-alive"
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
For HTTP servers:
|
|
74
|
+
|
|
75
|
+
```json
|
|
76
|
+
{
|
|
77
|
+
"mcpServers": {
|
|
78
|
+
"remote-api": {
|
|
79
|
+
"url": "https://api.example.com/mcp",
|
|
80
|
+
"auth": "bearer",
|
|
81
|
+
"bearerTokenEnv": "API_TOKEN"
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Options
|
|
88
|
+
|
|
89
|
+
| Field | Description |
|
|
90
|
+
|-------|-------------|
|
|
91
|
+
| `command` | Executable for stdio transport |
|
|
92
|
+
| `args` | Command arguments |
|
|
93
|
+
| `env` | Environment variables (`${VAR}` interpolation supported) |
|
|
94
|
+
| `url` | HTTP endpoint (tries StreamableHTTP, falls back to SSE) |
|
|
95
|
+
| `auth` | `"bearer"` or `"oauth"` |
|
|
96
|
+
| `bearerToken` / `bearerTokenEnv` | Token or env var containing token |
|
|
97
|
+
| `lifecycle` | `"keep-alive"` for auto-reconnect |
|
|
98
|
+
| `exposeResources` | Expose MCP resources as tools (default: true) |
|
|
99
|
+
| `debug` | Show server stderr output (default: false) |
|
|
100
|
+
|
|
101
|
+
### Import Existing Configs
|
|
102
|
+
|
|
103
|
+
Already have MCP set up in Cursor or Claude? Import it:
|
|
104
|
+
|
|
105
|
+
```json
|
|
106
|
+
{
|
|
107
|
+
"imports": ["cursor", "claude-code", "claude-desktop"],
|
|
108
|
+
"mcpServers": { }
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Supported: `cursor`, `claude-code`, `claude-desktop`, `vscode`, `windsurf`, `codex`
|
|
113
|
+
|
|
114
|
+
## Usage
|
|
115
|
+
|
|
116
|
+
### Search
|
|
117
|
+
|
|
118
|
+
```
|
|
119
|
+
mcp({ search: "screenshot navigate" })
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Space-separated words are OR'd. Results include parameter schemas by default.
|
|
123
|
+
|
|
124
|
+
Use `includeSchemas: false` for compact output, `regex: true` for regex matching.
|
|
125
|
+
|
|
126
|
+
### Describe
|
|
127
|
+
|
|
128
|
+
```
|
|
129
|
+
mcp({ describe: "chrome_devtools_take_screenshot" })
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Full details for a specific tool. Mostly redundant now that search includes schemas.
|
|
133
|
+
|
|
134
|
+
### Call
|
|
135
|
+
|
|
136
|
+
```
|
|
137
|
+
mcp({ tool: "chrome_devtools_navigate_page", args: { type: "url", url: "https://example.com" } })
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
If you pass bad arguments, the error includes the expected schema.
|
|
141
|
+
|
|
142
|
+
### Status
|
|
143
|
+
|
|
144
|
+
```
|
|
145
|
+
mcp({ })
|
|
146
|
+
mcp({ server: "chrome-devtools" })
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
See connected servers and their tools.
|
|
150
|
+
|
|
151
|
+
## Commands
|
|
152
|
+
|
|
153
|
+
| Command | What it does |
|
|
154
|
+
|---------|--------------|
|
|
155
|
+
| `/mcp` | Server status |
|
|
156
|
+
| `/mcp tools` | List all tools |
|
|
157
|
+
| `/mcp reconnect` | Reconnect all servers |
|
|
158
|
+
| `/mcp-auth <server>` | OAuth setup instructions |
|
|
159
|
+
|
|
160
|
+
## OAuth
|
|
161
|
+
|
|
162
|
+
For OAuth servers, get a token from your provider and save it:
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
mkdir -p ~/.pi/agent/mcp-oauth/my-server
|
|
166
|
+
cat > ~/.pi/agent/mcp-oauth/my-server/tokens.json << 'EOF'
|
|
167
|
+
{
|
|
168
|
+
"access_token": "your-token",
|
|
169
|
+
"token_type": "bearer"
|
|
170
|
+
}
|
|
171
|
+
EOF
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Then `/mcp reconnect`.
|
|
175
|
+
|
|
176
|
+
## How It Works
|
|
177
|
+
|
|
178
|
+
See [ARCHITECTURE.md](./ARCHITECTURE.md) for the full breakdown. The short version:
|
|
179
|
+
|
|
180
|
+
- One `mcp` tool registered with Pi (~200 tokens)
|
|
181
|
+
- Tool metadata stored in a map, looked up at call time
|
|
182
|
+
- MCP server validates arguments (no schema conversion needed)
|
|
183
|
+
- Keep-alive servers get health checks and auto-reconnect
|
|
184
|
+
|
|
185
|
+
## Limitations
|
|
186
|
+
|
|
187
|
+
- OAuth tokens obtained externally (no browser flow)
|
|
188
|
+
- No automatic token refresh
|
|
189
|
+
- Servers connect sequentially on startup
|
package/config.ts
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// config.ts - Config loading with import support
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { join, resolve } from "node:path";
|
|
5
|
+
import type { McpConfig, ServerEntry, McpSettings, ImportKind } from "./types.js";
|
|
6
|
+
|
|
7
|
+
const DEFAULT_CONFIG_PATH = join(homedir(), ".pi", "agent", "mcp.json");
|
|
8
|
+
const PROJECT_CONFIG_NAME = ".pi/mcp.json";
|
|
9
|
+
|
|
10
|
+
// Import source paths for other tools
|
|
11
|
+
const IMPORT_PATHS: Record<ImportKind, string> = {
|
|
12
|
+
"cursor": join(homedir(), ".cursor", "mcp.json"),
|
|
13
|
+
"claude-code": join(homedir(), ".claude", "claude_desktop_config.json"),
|
|
14
|
+
"claude-desktop": join(homedir(), "Library", "Application Support", "Claude", "claude_desktop_config.json"),
|
|
15
|
+
"codex": join(homedir(), ".codex", "config.json"),
|
|
16
|
+
"windsurf": join(homedir(), ".windsurf", "mcp.json"),
|
|
17
|
+
"vscode": ".vscode/mcp.json", // Relative to project
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export function loadMcpConfig(overridePath?: string): McpConfig {
|
|
21
|
+
const configPath = overridePath ? resolve(overridePath) : DEFAULT_CONFIG_PATH;
|
|
22
|
+
|
|
23
|
+
// Load base config
|
|
24
|
+
let config: McpConfig = { mcpServers: {} };
|
|
25
|
+
|
|
26
|
+
if (existsSync(configPath)) {
|
|
27
|
+
try {
|
|
28
|
+
const raw = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
29
|
+
config = validateConfig(raw);
|
|
30
|
+
} catch (error) {
|
|
31
|
+
console.warn(`Failed to load MCP config from ${configPath}:`, error);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Process imports from other tools
|
|
36
|
+
if (config.imports?.length) {
|
|
37
|
+
for (const importKind of config.imports) {
|
|
38
|
+
const importPath = IMPORT_PATHS[importKind];
|
|
39
|
+
if (!importPath) continue;
|
|
40
|
+
|
|
41
|
+
const fullPath = importPath.startsWith(".")
|
|
42
|
+
? resolve(process.cwd(), importPath)
|
|
43
|
+
: importPath;
|
|
44
|
+
|
|
45
|
+
if (!existsSync(fullPath)) continue;
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
const imported = JSON.parse(readFileSync(fullPath, "utf-8"));
|
|
49
|
+
const servers = extractServers(imported, importKind);
|
|
50
|
+
|
|
51
|
+
// Merge - local config takes precedence over imports
|
|
52
|
+
for (const [name, def] of Object.entries(servers)) {
|
|
53
|
+
if (!config.mcpServers[name]) {
|
|
54
|
+
config.mcpServers[name] = def;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
} catch (error) {
|
|
58
|
+
console.warn(`Failed to import MCP config from ${importKind}:`, error);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Check for project-local config (skip if it's the same as the main config)
|
|
64
|
+
const projectPath = resolve(process.cwd(), PROJECT_CONFIG_NAME);
|
|
65
|
+
if (existsSync(projectPath) && projectPath !== configPath) {
|
|
66
|
+
try {
|
|
67
|
+
const projectConfig = JSON.parse(readFileSync(projectPath, "utf-8"));
|
|
68
|
+
const validated = validateConfig(projectConfig);
|
|
69
|
+
|
|
70
|
+
// Project config overrides everything
|
|
71
|
+
config.mcpServers = { ...config.mcpServers, ...validated.mcpServers };
|
|
72
|
+
if (validated.settings) {
|
|
73
|
+
config.settings = { ...config.settings, ...validated.settings };
|
|
74
|
+
}
|
|
75
|
+
} catch (error) {
|
|
76
|
+
console.warn(`Failed to load project MCP config:`, error);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return config;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function validateConfig(raw: unknown): McpConfig {
|
|
84
|
+
if (!raw || typeof raw !== "object") {
|
|
85
|
+
return { mcpServers: {} };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const obj = raw as Record<string, unknown>;
|
|
89
|
+
const servers = obj.mcpServers ?? obj["mcp-servers"] ?? {};
|
|
90
|
+
|
|
91
|
+
// Must be a plain object, not an array or null
|
|
92
|
+
if (typeof servers !== "object" || servers === null || Array.isArray(servers)) {
|
|
93
|
+
return { mcpServers: {} };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
mcpServers: servers as Record<string, ServerEntry>,
|
|
98
|
+
imports: Array.isArray(obj.imports) ? obj.imports as ImportKind[] : undefined,
|
|
99
|
+
settings: obj.settings as McpSettings | undefined,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function extractServers(config: unknown, kind: ImportKind): Record<string, ServerEntry> {
|
|
104
|
+
if (!config || typeof config !== "object") return {};
|
|
105
|
+
|
|
106
|
+
const obj = config as Record<string, unknown>;
|
|
107
|
+
|
|
108
|
+
let servers: unknown;
|
|
109
|
+
switch (kind) {
|
|
110
|
+
case "claude-desktop":
|
|
111
|
+
case "claude-code":
|
|
112
|
+
servers = obj.mcpServers;
|
|
113
|
+
break;
|
|
114
|
+
case "cursor":
|
|
115
|
+
case "windsurf":
|
|
116
|
+
case "vscode":
|
|
117
|
+
servers = obj.mcpServers ?? obj["mcp-servers"];
|
|
118
|
+
break;
|
|
119
|
+
case "codex":
|
|
120
|
+
servers = obj.mcpServers;
|
|
121
|
+
break;
|
|
122
|
+
default:
|
|
123
|
+
return {};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Validate servers is a plain object
|
|
127
|
+
if (!servers || typeof servers !== "object" || Array.isArray(servers)) {
|
|
128
|
+
return {};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return servers as Record<string, ServerEntry>;
|
|
132
|
+
}
|