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/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
+ }