@oh-my-pi/pi-coding-agent 1.337.1 → 1.340.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/CHANGELOG.md +32 -0
- package/package.json +3 -3
- package/src/cli/args.ts +14 -8
- package/src/core/export-html/index.ts +48 -15
- package/src/core/export-html/template.html +3 -11
- package/src/core/mcp/client.ts +43 -16
- package/src/core/mcp/config.ts +152 -6
- package/src/core/mcp/index.ts +6 -2
- package/src/core/mcp/loader.ts +30 -3
- package/src/core/mcp/manager.ts +69 -10
- package/src/core/mcp/types.ts +9 -3
- package/src/core/sdk.ts +19 -3
- package/src/core/settings-manager.ts +34 -0
- package/src/modes/interactive/components/settings-defs.ts +229 -0
- package/src/modes/interactive/components/settings-selector.ts +156 -234
- package/src/modes/interactive/interactive-mode.ts +58 -75
- package/src/utils/shell.ts +12 -4
- package/src/utils/tools-manager.ts +5 -14
- package/src/core/export-html/vendor/highlight.min.js +0 -1213
- package/src/core/export-html/vendor/marked.min.js +0 -6
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,38 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [1.340.0] - 2026-01-03
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- Replaced vendored highlight.js and marked.js with CDN-hosted versions for smaller exports
|
|
10
|
+
- Added runtime minification for HTML, CSS, and JS in session exports
|
|
11
|
+
- Session share URL now uses gistpreview.github.io instead of shittycodingagent.ai
|
|
12
|
+
|
|
13
|
+
## [1.339.0] - 2026-01-03
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- MCP project config setting to disable loading `.mcp.json`/`mcp.json` from project root
|
|
18
|
+
- Support for both `mcp.json` and `.mcp.json` filenames (prefers `mcp.json` if both exist)
|
|
19
|
+
- Automatic Exa MCP server filtering with API key extraction for native integration
|
|
20
|
+
|
|
21
|
+
## [1.338.0] - 2026-01-03
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
|
|
25
|
+
- Bash interceptor setting to block shell commands that have dedicated tools (disabled by default, enable via `/settings`)
|
|
26
|
+
|
|
27
|
+
### Changed
|
|
28
|
+
|
|
29
|
+
- Refactored settings UI to declarative definitions for easier maintenance
|
|
30
|
+
- Shell detection now respects `$SHELL` environment variable before falling back to bash/sh
|
|
31
|
+
- Tool binary detection now uses `Bun.which()` instead of spawning processes
|
|
32
|
+
|
|
33
|
+
### Fixed
|
|
34
|
+
|
|
35
|
+
- CLI help text now accurately lists all default tools
|
|
36
|
+
|
|
5
37
|
## [1.337.1] - 2026-01-02
|
|
6
38
|
|
|
7
39
|
### Added
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oh-my-pi/pi-coding-agent",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.340.0",
|
|
4
4
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"piConfig": {
|
|
@@ -33,8 +33,8 @@
|
|
|
33
33
|
"clean": "rm -rf dist",
|
|
34
34
|
"build": "tsgo -p tsconfig.build.json && chmod +x dist/cli.js && npm run copy-assets",
|
|
35
35
|
"build:binary": "npm run build && bun build --compile ./dist/cli.js --outfile dist/pi && npm run copy-binary-assets",
|
|
36
|
-
"copy-assets": "mkdir -p dist/modes/interactive/theme && cp src/modes/interactive/theme/*.json dist/modes/interactive/theme/ && mkdir -p dist/core/export-html
|
|
37
|
-
"copy-binary-assets": "cp package.json dist/ && cp README.md dist/ && cp CHANGELOG.md dist/ && mkdir -p dist/theme && cp src/modes/interactive/theme/*.json dist/theme/ && mkdir -p dist/export-html
|
|
36
|
+
"copy-assets": "mkdir -p dist/modes/interactive/theme && cp src/modes/interactive/theme/*.json dist/modes/interactive/theme/ && mkdir -p dist/core/export-html && cp src/core/export-html/template.html src/core/export-html/template.css src/core/export-html/template.js dist/core/export-html/",
|
|
37
|
+
"copy-binary-assets": "cp package.json dist/ && cp README.md dist/ && cp CHANGELOG.md dist/ && mkdir -p dist/theme && cp src/modes/interactive/theme/*.json dist/theme/ && mkdir -p dist/export-html && cp src/core/export-html/template.html src/core/export-html/template.css src/core/export-html/template.js dist/export-html/ && cp -r docs dist/ && cp -r examples dist/",
|
|
38
38
|
"test": "vitest --run",
|
|
39
39
|
"prepublishOnly": "npm run clean && npm run build"
|
|
40
40
|
},
|
package/src/cli/args.ts
CHANGED
|
@@ -234,13 +234,19 @@ ${chalk.bold("Environment Variables:")}
|
|
|
234
234
|
${chalk.dim("# Configuration")}
|
|
235
235
|
${ENV_AGENT_DIR.padEnd(23)} - Session storage directory (default: ~/${CONFIG_DIR_NAME}/agent)
|
|
236
236
|
|
|
237
|
-
${chalk.bold("Available Tools (
|
|
238
|
-
read
|
|
239
|
-
bash
|
|
240
|
-
edit
|
|
241
|
-
write
|
|
242
|
-
grep
|
|
243
|
-
find
|
|
244
|
-
ls
|
|
237
|
+
${chalk.bold("Available Tools (all enabled by default):")}
|
|
238
|
+
read - Read file contents
|
|
239
|
+
bash - Execute bash commands
|
|
240
|
+
edit - Edit files with find/replace
|
|
241
|
+
write - Write files (creates/overwrites)
|
|
242
|
+
grep - Search file contents
|
|
243
|
+
find - Find files by glob pattern
|
|
244
|
+
ls - List directory contents
|
|
245
|
+
lsp - Language server protocol (code intelligence)
|
|
246
|
+
notebook - Edit Jupyter notebooks
|
|
247
|
+
task - Launch sub-agents for parallel tasks
|
|
248
|
+
web_fetch - Fetch and process web pages
|
|
249
|
+
web_search - Search the web
|
|
250
|
+
ask - Ask user questions (interactive mode only)
|
|
245
251
|
`);
|
|
246
252
|
}
|
|
@@ -5,6 +5,34 @@ import { APP_NAME, getExportTemplateDir } from "../../config.js";
|
|
|
5
5
|
import { getResolvedThemeColors, getThemeExportColors } from "../../modes/interactive/theme/theme.js";
|
|
6
6
|
import { SessionManager } from "../session-manager.js";
|
|
7
7
|
|
|
8
|
+
// Cached minified assets (populated on first use)
|
|
9
|
+
let cachedTemplate: string | null = null;
|
|
10
|
+
let cachedJs: string | null = null;
|
|
11
|
+
|
|
12
|
+
/** Minify CSS by removing comments, unnecessary whitespace, and newlines. */
|
|
13
|
+
function minifyCss(css: string): string {
|
|
14
|
+
return css
|
|
15
|
+
.replace(/\/\*[\s\S]*?\*\//g, "") // Remove comments
|
|
16
|
+
.replace(/\s+/g, " ") // Collapse whitespace
|
|
17
|
+
.replace(/\s*([{}:;,>+~])\s*/g, "$1") // Remove space around punctuation
|
|
18
|
+
.replace(/;}/g, "}") // Remove trailing semicolons
|
|
19
|
+
.trim();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Minify JS using Bun's transpiler. */
|
|
23
|
+
function minifyJs(js: string): string {
|
|
24
|
+
const transpiler = new Bun.Transpiler({ loader: "js", minifyWhitespace: true });
|
|
25
|
+
return transpiler.transformSync(js);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Minify HTML by collapsing whitespace outside of tags. */
|
|
29
|
+
function minifyHtml(html: string): string {
|
|
30
|
+
return html
|
|
31
|
+
.replace(/>\s+</g, "><") // Remove whitespace between tags
|
|
32
|
+
.replace(/\s{2,}/g, " ") // Collapse multiple spaces
|
|
33
|
+
.trim();
|
|
34
|
+
}
|
|
35
|
+
|
|
8
36
|
export interface ExportOptions {
|
|
9
37
|
outputPath?: string;
|
|
10
38
|
themeName?: string;
|
|
@@ -111,11 +139,16 @@ interface SessionData {
|
|
|
111
139
|
*/
|
|
112
140
|
function generateHtml(sessionData: SessionData, themeName?: string): string {
|
|
113
141
|
const templateDir = getExportTemplateDir();
|
|
114
|
-
|
|
142
|
+
|
|
143
|
+
// Load and minify assets on first use
|
|
144
|
+
if (!cachedTemplate) {
|
|
145
|
+
cachedTemplate = minifyHtml(readFileSync(join(templateDir, "template.html"), "utf-8"));
|
|
146
|
+
}
|
|
147
|
+
if (!cachedJs) {
|
|
148
|
+
cachedJs = minifyJs(readFileSync(join(templateDir, "template.js"), "utf-8"));
|
|
149
|
+
}
|
|
150
|
+
|
|
115
151
|
const templateCss = readFileSync(join(templateDir, "template.css"), "utf-8");
|
|
116
|
-
const templateJs = readFileSync(join(templateDir, "template.js"), "utf-8");
|
|
117
|
-
const markedJs = readFileSync(join(templateDir, "vendor", "marked.min.js"), "utf-8");
|
|
118
|
-
const hljsJs = readFileSync(join(templateDir, "vendor", "highlight.min.js"), "utf-8");
|
|
119
152
|
|
|
120
153
|
const themeVars = generateThemeVars(themeName);
|
|
121
154
|
const colors = getResolvedThemeColors(themeName);
|
|
@@ -127,19 +160,19 @@ function generateHtml(sessionData: SessionData, themeName?: string): string {
|
|
|
127
160
|
// Base64 encode session data to avoid escaping issues
|
|
128
161
|
const sessionDataBase64 = Buffer.from(JSON.stringify(sessionData)).toString("base64");
|
|
129
162
|
|
|
130
|
-
// Build the CSS with theme variables injected
|
|
131
|
-
const css =
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
163
|
+
// Build and minify the CSS with theme variables injected
|
|
164
|
+
const css = minifyCss(
|
|
165
|
+
templateCss
|
|
166
|
+
.replace("{{THEME_VARS}}", themeVars)
|
|
167
|
+
.replace("{{BODY_BG}}", bodyBg)
|
|
168
|
+
.replace("{{CONTAINER_BG}}", containerBg)
|
|
169
|
+
.replace("{{INFO_BG}}", infoBg),
|
|
170
|
+
);
|
|
136
171
|
|
|
137
|
-
return
|
|
172
|
+
return cachedTemplate
|
|
138
173
|
.replace("{{CSS}}", css)
|
|
139
|
-
.replace("{{JS}}",
|
|
140
|
-
.replace("{{SESSION_DATA}}", sessionDataBase64)
|
|
141
|
-
.replace("{{MARKED_JS}}", markedJs)
|
|
142
|
-
.replace("{{HIGHLIGHT_JS}}", hljsJs);
|
|
174
|
+
.replace("{{JS}}", cachedJs)
|
|
175
|
+
.replace("{{SESSION_DATA}}", sessionDataBase64);
|
|
143
176
|
}
|
|
144
177
|
|
|
145
178
|
/**
|
|
@@ -39,16 +39,8 @@
|
|
|
39
39
|
</div>
|
|
40
40
|
|
|
41
41
|
<script id="session-data" type="application/json">{{SESSION_DATA}}</script>
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
<script>{{
|
|
45
|
-
|
|
46
|
-
<!-- highlight.js -->
|
|
47
|
-
<script>{{HIGHLIGHT_JS}}</script>
|
|
48
|
-
|
|
49
|
-
<!-- Main application code -->
|
|
50
|
-
<script>
|
|
51
|
-
{{JS}}
|
|
52
|
-
</script>
|
|
42
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/marked/15.0.4/marked.min.js" integrity="sha512-VmLxPVdDGeR+F0DzUHVqzHwaR4ZSSh1g/7aYXwKT1PAGVxunOEcysta+4H5Utvmpr2xExEPybZ8q+iM9F1tGdw==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
|
43
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js" integrity="sha512-D9gUyxqja7hBtkWpPWGt9wfbfaMGVt9gnyCvYa+jojwwPHLCzUm5i8rpk7vD7wNee9bA35eYIjobYPaQuKS1MQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
|
44
|
+
<script>{{JS}}</script>
|
|
53
45
|
</body>
|
|
54
46
|
</html>
|
package/src/core/mcp/client.ts
CHANGED
|
@@ -25,12 +25,32 @@ import type {
|
|
|
25
25
|
/** MCP protocol version we support */
|
|
26
26
|
const PROTOCOL_VERSION = "2025-03-26";
|
|
27
27
|
|
|
28
|
+
/** Default connection timeout in ms */
|
|
29
|
+
const CONNECTION_TIMEOUT_MS = 30_000;
|
|
30
|
+
|
|
28
31
|
/** Client info sent during initialization */
|
|
29
32
|
const CLIENT_INFO = {
|
|
30
33
|
name: "pi-coding-agent",
|
|
31
34
|
version: "1.0.0",
|
|
32
35
|
};
|
|
33
36
|
|
|
37
|
+
/** Wrap a promise with a timeout */
|
|
38
|
+
function withTimeout<T>(promise: Promise<T>, ms: number, message: string): Promise<T> {
|
|
39
|
+
return new Promise((resolve, reject) => {
|
|
40
|
+
const timer = setTimeout(() => reject(new Error(message)), ms);
|
|
41
|
+
promise.then(
|
|
42
|
+
(value) => {
|
|
43
|
+
clearTimeout(timer);
|
|
44
|
+
resolve(value);
|
|
45
|
+
},
|
|
46
|
+
(error) => {
|
|
47
|
+
clearTimeout(timer);
|
|
48
|
+
reject(error);
|
|
49
|
+
},
|
|
50
|
+
);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
34
54
|
/**
|
|
35
55
|
* Create a transport for the given server config.
|
|
36
56
|
*/
|
|
@@ -73,24 +93,31 @@ async function initializeConnection(transport: MCPTransport): Promise<MCPInitial
|
|
|
73
93
|
|
|
74
94
|
/**
|
|
75
95
|
* Connect to an MCP server.
|
|
96
|
+
* Has a 30 second timeout to prevent blocking startup.
|
|
76
97
|
*/
|
|
77
98
|
export async function connectToServer(name: string, config: MCPServerConfig): Promise<MCPServerConnection> {
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
99
|
+
const timeoutMs = config.timeout ?? CONNECTION_TIMEOUT_MS;
|
|
100
|
+
|
|
101
|
+
const connect = async (): Promise<MCPServerConnection> => {
|
|
102
|
+
const transport = await createTransport(config);
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
const initResult = await initializeConnection(transport);
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
name,
|
|
109
|
+
config,
|
|
110
|
+
transport,
|
|
111
|
+
serverInfo: initResult.serverInfo,
|
|
112
|
+
capabilities: initResult.capabilities,
|
|
113
|
+
};
|
|
114
|
+
} catch (error) {
|
|
115
|
+
await transport.close();
|
|
116
|
+
throw error;
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
return withTimeout(connect(), timeoutMs, `Connection to MCP server "${name}" timed out after ${timeoutMs}ms`);
|
|
94
121
|
}
|
|
95
122
|
|
|
96
123
|
/**
|
package/src/core/mcp/config.ts
CHANGED
|
@@ -91,11 +91,17 @@ export interface MCPConfigLocations {
|
|
|
91
91
|
*/
|
|
92
92
|
export function getMCPConfigPaths(cwd: string): MCPConfigLocations {
|
|
93
93
|
const home = homedir();
|
|
94
|
+
|
|
95
|
+
// Project-level: check both mcp.json and .mcp.json (prefer mcp.json if both exist)
|
|
96
|
+
const mcpJson = join(cwd, "mcp.json");
|
|
97
|
+
const dotMcpJson = join(cwd, ".mcp.json");
|
|
98
|
+
const projectPath = existsSync(mcpJson) ? mcpJson : dotMcpJson;
|
|
99
|
+
|
|
94
100
|
return {
|
|
95
101
|
// User-level: ~/.pi/mcp.json (our standard)
|
|
96
102
|
user: join(home, ".pi", "mcp.json"),
|
|
97
|
-
// Project-level: .mcp.json at project root
|
|
98
|
-
project:
|
|
103
|
+
// Project-level: mcp.json or .mcp.json at project root
|
|
104
|
+
project: projectPath,
|
|
99
105
|
};
|
|
100
106
|
}
|
|
101
107
|
|
|
@@ -115,17 +121,157 @@ export function mergeMCPConfigs(...configs: (MCPConfigFile | null)[]): Record<st
|
|
|
115
121
|
return result;
|
|
116
122
|
}
|
|
117
123
|
|
|
124
|
+
/** Options for loading MCP configs */
|
|
125
|
+
export interface LoadMCPConfigsOptions {
|
|
126
|
+
/** Additional environment variables for expansion */
|
|
127
|
+
extraEnv?: Record<string, string>;
|
|
128
|
+
/** Whether to load project-level config (default: true) */
|
|
129
|
+
enableProjectConfig?: boolean;
|
|
130
|
+
/** Whether to filter out Exa MCP servers (default: true) */
|
|
131
|
+
filterExa?: boolean;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Result of loading MCP configs */
|
|
135
|
+
export interface LoadMCPConfigsResult {
|
|
136
|
+
/** Loaded server configs */
|
|
137
|
+
configs: Record<string, MCPServerConfig>;
|
|
138
|
+
/** Extracted Exa API keys (if any were filtered) */
|
|
139
|
+
exaApiKeys: string[];
|
|
140
|
+
}
|
|
141
|
+
|
|
118
142
|
/**
|
|
119
143
|
* Load all MCP server configs from standard locations.
|
|
120
144
|
* Returns merged config with project overriding user.
|
|
145
|
+
*
|
|
146
|
+
* @param cwd Working directory (project root)
|
|
147
|
+
* @param options Load options or extraEnv for backwards compatibility
|
|
121
148
|
*/
|
|
122
|
-
export function loadAllMCPConfigs(
|
|
149
|
+
export function loadAllMCPConfigs(
|
|
150
|
+
cwd: string,
|
|
151
|
+
options?: LoadMCPConfigsOptions | Record<string, string>,
|
|
152
|
+
): LoadMCPConfigsResult {
|
|
153
|
+
// Support old signature: loadAllMCPConfigs(cwd, extraEnv)
|
|
154
|
+
const opts: LoadMCPConfigsOptions =
|
|
155
|
+
options && ("extraEnv" in options || "enableProjectConfig" in options || "filterExa" in options)
|
|
156
|
+
? (options as LoadMCPConfigsOptions)
|
|
157
|
+
: { extraEnv: options as Record<string, string> | undefined };
|
|
158
|
+
|
|
159
|
+
const enableProjectConfig = opts.enableProjectConfig ?? true;
|
|
160
|
+
const filterExa = opts.filterExa ?? true;
|
|
161
|
+
|
|
123
162
|
const paths = getMCPConfigPaths(cwd);
|
|
124
163
|
|
|
125
|
-
const userConfig = paths.user ? loadMCPConfigFile(paths.user, extraEnv) : null;
|
|
126
|
-
const projectConfig = paths.project ? loadMCPConfigFile(paths.project, extraEnv) : null;
|
|
164
|
+
const userConfig = paths.user ? loadMCPConfigFile(paths.user, opts.extraEnv) : null;
|
|
165
|
+
const projectConfig = enableProjectConfig && paths.project ? loadMCPConfigFile(paths.project, opts.extraEnv) : null;
|
|
166
|
+
|
|
167
|
+
let configs = mergeMCPConfigs(userConfig, projectConfig);
|
|
168
|
+
let exaApiKeys: string[] = [];
|
|
169
|
+
|
|
170
|
+
if (filterExa) {
|
|
171
|
+
const result = filterExaMCPServers(configs);
|
|
172
|
+
configs = result.configs;
|
|
173
|
+
exaApiKeys = result.exaApiKeys;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return { configs, exaApiKeys };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Pattern to match Exa MCP servers */
|
|
180
|
+
const EXA_MCP_URL_PATTERN = /mcp\.exa\.ai/i;
|
|
181
|
+
const EXA_API_KEY_PATTERN = /exaApiKey=([^&\s]+)/i;
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Check if a server config is an Exa MCP server.
|
|
185
|
+
*/
|
|
186
|
+
export function isExaMCPServer(name: string, config: MCPServerConfig): boolean {
|
|
187
|
+
// Check by server name
|
|
188
|
+
if (name.toLowerCase() === "exa") {
|
|
189
|
+
return true;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Check by URL for HTTP/SSE servers
|
|
193
|
+
if (config.type === "http" || config.type === "sse") {
|
|
194
|
+
const httpConfig = config as { url?: string };
|
|
195
|
+
if (httpConfig.url && EXA_MCP_URL_PATTERN.test(httpConfig.url)) {
|
|
196
|
+
return true;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Check by args for stdio servers (e.g., mcp-remote to exa)
|
|
201
|
+
if (!config.type || config.type === "stdio") {
|
|
202
|
+
const stdioConfig = config as { args?: string[] };
|
|
203
|
+
if (stdioConfig.args?.some((arg) => EXA_MCP_URL_PATTERN.test(arg))) {
|
|
204
|
+
return true;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Extract Exa API key from an MCP server config.
|
|
213
|
+
*/
|
|
214
|
+
export function extractExaApiKey(config: MCPServerConfig): string | undefined {
|
|
215
|
+
// Check URL for HTTP/SSE servers
|
|
216
|
+
if (config.type === "http" || config.type === "sse") {
|
|
217
|
+
const httpConfig = config as { url?: string };
|
|
218
|
+
if (httpConfig.url) {
|
|
219
|
+
const match = EXA_API_KEY_PATTERN.exec(httpConfig.url);
|
|
220
|
+
if (match) return match[1];
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Check args for stdio servers
|
|
225
|
+
if (!config.type || config.type === "stdio") {
|
|
226
|
+
const stdioConfig = config as { args?: string[] };
|
|
227
|
+
if (stdioConfig.args) {
|
|
228
|
+
for (const arg of stdioConfig.args) {
|
|
229
|
+
const match = EXA_API_KEY_PATTERN.exec(arg);
|
|
230
|
+
if (match) return match[1];
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Check env vars
|
|
236
|
+
if ("env" in config && config.env) {
|
|
237
|
+
const envConfig = config as { env: Record<string, string> };
|
|
238
|
+
if (envConfig.env.EXA_API_KEY) {
|
|
239
|
+
return envConfig.env.EXA_API_KEY;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return undefined;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** Result of filtering Exa MCP servers */
|
|
247
|
+
export interface ExaFilterResult {
|
|
248
|
+
/** Configs with Exa servers removed */
|
|
249
|
+
configs: Record<string, MCPServerConfig>;
|
|
250
|
+
/** Extracted Exa API keys (if any) */
|
|
251
|
+
exaApiKeys: string[];
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Filter out Exa MCP servers and extract their API keys.
|
|
256
|
+
* Since we have native Exa integration, we don't need the MCP server.
|
|
257
|
+
*/
|
|
258
|
+
export function filterExaMCPServers(configs: Record<string, MCPServerConfig>): ExaFilterResult {
|
|
259
|
+
const filtered: Record<string, MCPServerConfig> = {};
|
|
260
|
+
const exaApiKeys: string[] = [];
|
|
261
|
+
|
|
262
|
+
for (const [name, config] of Object.entries(configs)) {
|
|
263
|
+
if (isExaMCPServer(name, config)) {
|
|
264
|
+
// Extract API key before filtering
|
|
265
|
+
const apiKey = extractExaApiKey(config);
|
|
266
|
+
if (apiKey) {
|
|
267
|
+
exaApiKeys.push(apiKey);
|
|
268
|
+
}
|
|
269
|
+
} else {
|
|
270
|
+
filtered[name] = config;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
127
273
|
|
|
128
|
-
return
|
|
274
|
+
return { configs: filtered, exaApiKeys };
|
|
129
275
|
}
|
|
130
276
|
|
|
131
277
|
/**
|
package/src/core/mcp/index.ts
CHANGED
|
@@ -9,19 +9,23 @@
|
|
|
9
9
|
export { callTool, connectToServer, disconnectServer, listTools, serverSupportsTools } from "./client.js";
|
|
10
10
|
|
|
11
11
|
// Config
|
|
12
|
+
export type { ExaFilterResult, LoadMCPConfigsOptions, LoadMCPConfigsResult } from "./config.js";
|
|
12
13
|
export {
|
|
13
14
|
expandEnvVars,
|
|
15
|
+
extractExaApiKey,
|
|
16
|
+
filterExaMCPServers,
|
|
14
17
|
getMCPConfigPaths,
|
|
18
|
+
isExaMCPServer,
|
|
15
19
|
loadAllMCPConfigs,
|
|
16
20
|
loadMCPConfigFile,
|
|
17
21
|
mergeMCPConfigs,
|
|
18
22
|
validateServerConfig,
|
|
19
23
|
} from "./config.js";
|
|
20
24
|
// Loader (for SDK integration)
|
|
21
|
-
export type { MCPToolsLoadResult } from "./loader.js";
|
|
25
|
+
export type { MCPToolsLoadOptions, MCPToolsLoadResult } from "./loader.js";
|
|
22
26
|
export { discoverAndLoadMCPTools } from "./loader.js";
|
|
23
27
|
// Manager
|
|
24
|
-
export type { MCPLoadResult } from "./manager.js";
|
|
28
|
+
export type { MCPDiscoverOptions, MCPLoadResult } from "./manager.js";
|
|
25
29
|
export { createMCPManager, MCPManager } from "./manager.js";
|
|
26
30
|
// Tool bridge
|
|
27
31
|
export type { MCPToolDetails } from "./tool-bridge.js";
|
package/src/core/mcp/loader.ts
CHANGED
|
@@ -17,24 +17,49 @@ export interface MCPToolsLoadResult {
|
|
|
17
17
|
errors: Array<{ path: string; error: string }>;
|
|
18
18
|
/** Connected server names */
|
|
19
19
|
connectedServers: string[];
|
|
20
|
+
/** Extracted Exa API keys from filtered MCP servers */
|
|
21
|
+
exaApiKeys: string[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Options for loading MCP tools */
|
|
25
|
+
export interface MCPToolsLoadOptions {
|
|
26
|
+
/** Additional environment variables for expansion */
|
|
27
|
+
extraEnv?: Record<string, string>;
|
|
28
|
+
/** Called when starting to connect to servers */
|
|
29
|
+
onConnecting?: (serverNames: string[]) => void;
|
|
30
|
+
/** Whether to load project-level config (default: true) */
|
|
31
|
+
enableProjectConfig?: boolean;
|
|
32
|
+
/** Whether to filter out Exa MCP servers (default: true) */
|
|
33
|
+
filterExa?: boolean;
|
|
20
34
|
}
|
|
21
35
|
|
|
22
36
|
/**
|
|
23
37
|
* Discover and load MCP tools from .mcp.json files.
|
|
24
38
|
*
|
|
25
39
|
* @param cwd Working directory (project root)
|
|
26
|
-
* @param
|
|
40
|
+
* @param options Load options including extraEnv and progress callbacks
|
|
27
41
|
* @returns MCP tools in LoadedCustomTool format for integration
|
|
28
42
|
*/
|
|
29
43
|
export async function discoverAndLoadMCPTools(
|
|
30
44
|
cwd: string,
|
|
31
|
-
|
|
45
|
+
options?: MCPToolsLoadOptions | Record<string, string>,
|
|
32
46
|
): Promise<MCPToolsLoadResult> {
|
|
47
|
+
// Support old signature: discoverAndLoadMCPTools(cwd, extraEnv)
|
|
48
|
+
const opts: MCPToolsLoadOptions =
|
|
49
|
+
options && ("extraEnv" in options || "onConnecting" in options || "enableProjectConfig" in options)
|
|
50
|
+
? (options as MCPToolsLoadOptions)
|
|
51
|
+
: { extraEnv: options as Record<string, string> | undefined };
|
|
52
|
+
|
|
33
53
|
const manager = new MCPManager(cwd);
|
|
34
54
|
|
|
35
55
|
let result: MCPLoadResult;
|
|
36
56
|
try {
|
|
37
|
-
result = await manager.discoverAndConnect(
|
|
57
|
+
result = await manager.discoverAndConnect({
|
|
58
|
+
extraEnv: opts.extraEnv,
|
|
59
|
+
onConnecting: opts.onConnecting,
|
|
60
|
+
enableProjectConfig: opts.enableProjectConfig,
|
|
61
|
+
filterExa: opts.filterExa,
|
|
62
|
+
});
|
|
38
63
|
} catch (error) {
|
|
39
64
|
// If discovery fails entirely, return empty result
|
|
40
65
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -43,6 +68,7 @@ export async function discoverAndLoadMCPTools(
|
|
|
43
68
|
tools: [],
|
|
44
69
|
errors: [{ path: ".mcp.json", error: message }],
|
|
45
70
|
connectedServers: [],
|
|
71
|
+
exaApiKeys: [],
|
|
46
72
|
};
|
|
47
73
|
}
|
|
48
74
|
|
|
@@ -64,5 +90,6 @@ export async function discoverAndLoadMCPTools(
|
|
|
64
90
|
tools: loadedTools,
|
|
65
91
|
errors,
|
|
66
92
|
connectedServers: result.connectedServers,
|
|
93
|
+
exaApiKeys: result.exaApiKeys,
|
|
67
94
|
};
|
|
68
95
|
}
|
package/src/core/mcp/manager.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import type { TSchema } from "@sinclair/typebox";
|
|
9
9
|
import type { CustomTool } from "../custom-tools/types.js";
|
|
10
10
|
import { connectToServer, disconnectServer, listTools } from "./client.js";
|
|
11
|
-
import { loadAllMCPConfigs, validateServerConfig } from "./config.js";
|
|
11
|
+
import { type LoadMCPConfigsOptions, loadAllMCPConfigs, validateServerConfig } from "./config.js";
|
|
12
12
|
import type { MCPToolDetails } from "./tool-bridge.js";
|
|
13
13
|
import { createMCPTools } from "./tool-bridge.js";
|
|
14
14
|
import type { MCPServerConfig, MCPServerConnection } from "./types.js";
|
|
@@ -21,6 +21,14 @@ export interface MCPLoadResult {
|
|
|
21
21
|
errors: Map<string, string>;
|
|
22
22
|
/** Connected server names */
|
|
23
23
|
connectedServers: string[];
|
|
24
|
+
/** Extracted Exa API keys from filtered MCP servers */
|
|
25
|
+
exaApiKeys: string[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Options for discovering and connecting to MCP servers */
|
|
29
|
+
export interface MCPDiscoverOptions extends LoadMCPConfigsOptions {
|
|
30
|
+
/** Called when starting to connect to servers */
|
|
31
|
+
onConnecting?: (serverNames: string[]) => void;
|
|
24
32
|
}
|
|
25
33
|
|
|
26
34
|
/**
|
|
@@ -38,19 +46,49 @@ export class MCPManager {
|
|
|
38
46
|
* Discover and connect to all MCP servers from .mcp.json files.
|
|
39
47
|
* Returns tools and any connection errors.
|
|
40
48
|
*/
|
|
41
|
-
async discoverAndConnect(
|
|
42
|
-
|
|
43
|
-
|
|
49
|
+
async discoverAndConnect(
|
|
50
|
+
extraEnvOrOptions?: Record<string, string> | MCPDiscoverOptions,
|
|
51
|
+
onConnecting?: (serverNames: string[]) => void,
|
|
52
|
+
): Promise<MCPLoadResult> {
|
|
53
|
+
// Support old signature: discoverAndConnect(extraEnv, onConnecting)
|
|
54
|
+
const opts: MCPDiscoverOptions =
|
|
55
|
+
extraEnvOrOptions &&
|
|
56
|
+
("extraEnv" in extraEnvOrOptions ||
|
|
57
|
+
"enableProjectConfig" in extraEnvOrOptions ||
|
|
58
|
+
"filterExa" in extraEnvOrOptions ||
|
|
59
|
+
"onConnecting" in extraEnvOrOptions)
|
|
60
|
+
? (extraEnvOrOptions as MCPDiscoverOptions)
|
|
61
|
+
: { extraEnv: extraEnvOrOptions as Record<string, string> | undefined, onConnecting };
|
|
62
|
+
|
|
63
|
+
const { configs, exaApiKeys } = loadAllMCPConfigs(this.cwd, {
|
|
64
|
+
extraEnv: opts.extraEnv,
|
|
65
|
+
enableProjectConfig: opts.enableProjectConfig,
|
|
66
|
+
filterExa: opts.filterExa,
|
|
67
|
+
});
|
|
68
|
+
const result = await this.connectServers(configs, opts.onConnecting);
|
|
69
|
+
result.exaApiKeys = exaApiKeys;
|
|
70
|
+
return result;
|
|
44
71
|
}
|
|
45
72
|
|
|
46
73
|
/**
|
|
47
74
|
* Connect to specific MCP servers.
|
|
75
|
+
* Connections are made in parallel for faster startup.
|
|
48
76
|
*/
|
|
49
|
-
async connectServers(
|
|
77
|
+
async connectServers(
|
|
78
|
+
configs: Record<string, MCPServerConfig>,
|
|
79
|
+
onConnecting?: (serverNames: string[]) => void,
|
|
80
|
+
): Promise<MCPLoadResult> {
|
|
50
81
|
const errors = new Map<string, string>();
|
|
51
82
|
const connectedServers: string[] = [];
|
|
52
83
|
const allTools: CustomTool<TSchema, MCPToolDetails>[] = [];
|
|
53
84
|
|
|
85
|
+
// Prepare connection tasks
|
|
86
|
+
const connectionTasks: Array<{
|
|
87
|
+
name: string;
|
|
88
|
+
config: MCPServerConfig;
|
|
89
|
+
validationErrors: string[];
|
|
90
|
+
}> = [];
|
|
91
|
+
|
|
54
92
|
for (const [name, config] of Object.entries(configs)) {
|
|
55
93
|
// Skip if already connected
|
|
56
94
|
if (this.connections.has(name)) {
|
|
@@ -65,17 +103,37 @@ export class MCPManager {
|
|
|
65
103
|
continue;
|
|
66
104
|
}
|
|
67
105
|
|
|
68
|
-
|
|
106
|
+
connectionTasks.push({ name, config, validationErrors });
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Notify about servers we're connecting to
|
|
110
|
+
if (connectionTasks.length > 0 && onConnecting) {
|
|
111
|
+
onConnecting(connectionTasks.map((t) => t.name));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Connect to all servers in parallel
|
|
115
|
+
const results = await Promise.allSettled(
|
|
116
|
+
connectionTasks.map(async ({ name, config }) => {
|
|
69
117
|
const connection = await connectToServer(name, config);
|
|
118
|
+
const serverTools = await listTools(connection);
|
|
119
|
+
return { name, connection, serverTools };
|
|
120
|
+
}),
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
// Process results
|
|
124
|
+
for (let i = 0; i < results.length; i++) {
|
|
125
|
+
const result = results[i];
|
|
126
|
+
const { name } = connectionTasks[i];
|
|
127
|
+
|
|
128
|
+
if (result.status === "fulfilled") {
|
|
129
|
+
const { connection, serverTools } = result.value;
|
|
70
130
|
this.connections.set(name, connection);
|
|
71
131
|
connectedServers.push(name);
|
|
72
132
|
|
|
73
|
-
// Load tools from this server
|
|
74
|
-
const serverTools = await listTools(connection);
|
|
75
133
|
const customTools = createMCPTools(connection, serverTools);
|
|
76
134
|
allTools.push(...customTools);
|
|
77
|
-
}
|
|
78
|
-
const message =
|
|
135
|
+
} else {
|
|
136
|
+
const message = result.reason instanceof Error ? result.reason.message : String(result.reason);
|
|
79
137
|
errors.set(name, message);
|
|
80
138
|
}
|
|
81
139
|
}
|
|
@@ -87,6 +145,7 @@ export class MCPManager {
|
|
|
87
145
|
tools: allTools,
|
|
88
146
|
errors,
|
|
89
147
|
connectedServers,
|
|
148
|
+
exaApiKeys: [], // Will be populated by discoverAndConnect
|
|
90
149
|
};
|
|
91
150
|
}
|
|
92
151
|
|