@oh-my-pi/pi-coding-agent 2.3.1337 → 3.1.1337
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 +72 -34
- package/README.md +100 -100
- package/docs/compaction.md +8 -8
- package/docs/config-usage.md +113 -0
- package/docs/custom-tools.md +8 -8
- package/docs/extension-loading.md +58 -58
- package/docs/hooks.md +11 -11
- package/docs/rpc.md +4 -4
- package/docs/sdk.md +14 -14
- package/docs/session-tree-plan.md +1 -1
- package/docs/session.md +2 -2
- package/docs/skills.md +16 -16
- package/docs/theme.md +9 -9
- package/docs/tui.md +1 -1
- package/examples/README.md +1 -1
- package/examples/custom-tools/README.md +4 -4
- package/examples/custom-tools/subagent/README.md +13 -13
- package/examples/custom-tools/subagent/agents.ts +2 -2
- package/examples/custom-tools/subagent/index.ts +5 -5
- package/examples/hooks/README.md +3 -3
- package/examples/hooks/auto-commit-on-exit.ts +1 -1
- package/examples/hooks/custom-compaction.ts +1 -1
- package/examples/sdk/01-minimal.ts +1 -1
- package/examples/sdk/04-skills.ts +1 -1
- package/examples/sdk/05-tools.ts +1 -1
- package/examples/sdk/08-slash-commands.ts +1 -1
- package/examples/sdk/09-api-keys-and-oauth.ts +2 -2
- package/examples/sdk/README.md +2 -2
- package/package.json +13 -11
- package/src/capability/context-file.ts +40 -0
- package/src/capability/extension.ts +48 -0
- package/src/capability/hook.ts +40 -0
- package/src/capability/index.ts +616 -0
- package/src/capability/instruction.ts +37 -0
- package/src/capability/mcp.ts +52 -0
- package/src/capability/prompt.ts +35 -0
- package/src/capability/rule.ts +52 -0
- package/src/capability/settings.ts +35 -0
- package/src/capability/skill.ts +49 -0
- package/src/capability/slash-command.ts +40 -0
- package/src/capability/system-prompt.ts +35 -0
- package/src/capability/tool.ts +38 -0
- package/src/capability/types.ts +166 -0
- package/src/cli/args.ts +2 -2
- package/src/cli/plugin-cli.ts +24 -19
- package/src/cli/update-cli.ts +10 -10
- package/src/config.ts +290 -6
- package/src/core/auth-storage.ts +32 -9
- package/src/core/bash-executor.ts +1 -1
- package/src/core/custom-commands/loader.ts +44 -50
- package/src/core/custom-tools/index.ts +1 -0
- package/src/core/custom-tools/loader.ts +67 -69
- package/src/core/custom-tools/types.ts +10 -1
- package/src/core/hooks/loader.ts +13 -42
- package/src/core/index.ts +0 -1
- package/src/core/logger.ts +7 -7
- package/src/core/mcp/client.ts +1 -1
- package/src/core/mcp/config.ts +94 -146
- package/src/core/mcp/index.ts +0 -4
- package/src/core/mcp/loader.ts +26 -22
- package/src/core/mcp/manager.ts +18 -23
- package/src/core/mcp/tool-bridge.ts +9 -1
- package/src/core/mcp/types.ts +2 -0
- package/src/core/model-registry.ts +25 -8
- package/src/core/plugins/installer.ts +1 -1
- package/src/core/plugins/loader.ts +17 -11
- package/src/core/plugins/manager.ts +2 -2
- package/src/core/plugins/paths.ts +12 -7
- package/src/core/plugins/types.ts +3 -3
- package/src/core/sdk.ts +48 -27
- package/src/core/session-manager.ts +4 -4
- package/src/core/settings-manager.ts +45 -21
- package/src/core/skills.ts +208 -293
- package/src/core/slash-commands.ts +34 -165
- package/src/core/system-prompt.ts +58 -65
- package/src/core/timings.ts +2 -2
- package/src/core/tools/lsp/config.ts +38 -17
- package/src/core/tools/task/agents.ts +21 -0
- package/src/core/tools/task/artifacts.ts +1 -1
- package/src/core/tools/task/bundled-agents/reviewer.md +2 -1
- package/src/core/tools/task/bundled-agents/task.md +1 -0
- package/src/core/tools/task/commands.ts +30 -107
- package/src/core/tools/task/discovery.ts +75 -66
- package/src/core/tools/task/executor.ts +25 -10
- package/src/core/tools/task/index.ts +35 -10
- package/src/core/tools/task/model-resolver.ts +27 -25
- package/src/core/tools/task/types.ts +6 -2
- package/src/core/tools/web-fetch.ts +3 -3
- package/src/core/tools/web-search/auth.ts +40 -34
- package/src/core/tools/web-search/index.ts +1 -1
- package/src/core/tools/web-search/providers/anthropic.ts +1 -1
- package/src/discovery/agents-md.ts +75 -0
- package/src/discovery/builtin.ts +646 -0
- package/src/discovery/claude.ts +623 -0
- package/src/discovery/cline.ts +102 -0
- package/src/discovery/codex.ts +571 -0
- package/src/discovery/cursor.ts +264 -0
- package/src/discovery/gemini.ts +368 -0
- package/src/discovery/github.ts +120 -0
- package/src/discovery/helpers.test.ts +127 -0
- package/src/discovery/helpers.ts +249 -0
- package/src/discovery/index.ts +84 -0
- package/src/discovery/mcp-json.ts +127 -0
- package/src/discovery/vscode.ts +99 -0
- package/src/discovery/windsurf.ts +216 -0
- package/src/main.ts +14 -13
- package/src/migrations.ts +24 -3
- package/src/modes/interactive/components/hook-editor.ts +1 -1
- package/src/modes/interactive/components/plugin-settings.ts +1 -1
- package/src/modes/interactive/components/settings-defs.ts +38 -2
- package/src/modes/interactive/components/settings-selector.ts +1 -0
- package/src/modes/interactive/components/welcome.ts +2 -2
- package/src/modes/interactive/interactive-mode.ts +233 -16
- package/src/modes/interactive/theme/theme-schema.json +1 -1
- package/src/utils/clipboard.ts +1 -1
- package/src/utils/shell-snapshot.ts +2 -2
- package/src/utils/shell.ts +7 -7
package/src/core/mcp/config.ts
CHANGED
|
@@ -1,130 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* MCP configuration loader.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* Supports ${VAR} and ${VAR:-default} syntax.
|
|
4
|
+
* Uses the capability system to load MCP servers from multiple sources.
|
|
6
5
|
*/
|
|
7
6
|
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import type {
|
|
12
|
-
|
|
13
|
-
/** Environment variable expansion pattern: ${VAR} or ${VAR:-default} */
|
|
14
|
-
const ENV_VAR_PATTERN = /\$\{([^}:]+)(?::-([^}]*))?\}/g;
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Expand environment variables in a string.
|
|
18
|
-
* Supports ${VAR} and ${VAR:-default} syntax.
|
|
19
|
-
*/
|
|
20
|
-
export function expandEnvVars(value: string, extraEnv?: Record<string, string>): string {
|
|
21
|
-
return value.replace(ENV_VAR_PATTERN, (_, varName: string, defaultValue?: string) => {
|
|
22
|
-
const envValue = extraEnv?.[varName] ?? process.env[varName];
|
|
23
|
-
if (envValue !== undefined) {
|
|
24
|
-
return envValue;
|
|
25
|
-
}
|
|
26
|
-
if (defaultValue !== undefined) {
|
|
27
|
-
return defaultValue;
|
|
28
|
-
}
|
|
29
|
-
// If no value and no default, leave the placeholder (will likely cause an error later)
|
|
30
|
-
return `\${${varName}}`;
|
|
31
|
-
});
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Recursively expand environment variables in an object.
|
|
36
|
-
*/
|
|
37
|
-
function expandEnvVarsInObject<T>(obj: T, extraEnv?: Record<string, string>): T {
|
|
38
|
-
if (typeof obj === "string") {
|
|
39
|
-
return expandEnvVars(obj, extraEnv) as T;
|
|
40
|
-
}
|
|
41
|
-
if (Array.isArray(obj)) {
|
|
42
|
-
return obj.map((item) => expandEnvVarsInObject(item, extraEnv)) as T;
|
|
43
|
-
}
|
|
44
|
-
if (obj !== null && typeof obj === "object") {
|
|
45
|
-
const result: Record<string, unknown> = {};
|
|
46
|
-
for (const [key, value] of Object.entries(obj)) {
|
|
47
|
-
result[key] = expandEnvVarsInObject(value, extraEnv);
|
|
48
|
-
}
|
|
49
|
-
return result as T;
|
|
50
|
-
}
|
|
51
|
-
return obj;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Load and parse an .mcp.json file.
|
|
56
|
-
* Returns null if file doesn't exist or is invalid.
|
|
57
|
-
*/
|
|
58
|
-
export function loadMCPConfigFile(filePath: string, extraEnv?: Record<string, string>): MCPConfigFile | null {
|
|
59
|
-
if (!existsSync(filePath)) {
|
|
60
|
-
return null;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
try {
|
|
64
|
-
const content = readFileSync(filePath, "utf-8");
|
|
65
|
-
const parsed = JSON.parse(content) as MCPConfigFile;
|
|
66
|
-
|
|
67
|
-
// Expand environment variables in server configs
|
|
68
|
-
if (parsed.mcpServers) {
|
|
69
|
-
parsed.mcpServers = expandEnvVarsInObject(parsed.mcpServers, extraEnv);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
return parsed;
|
|
73
|
-
} catch (error) {
|
|
74
|
-
console.error(`Warning: Failed to parse ${filePath}: ${error}`);
|
|
75
|
-
return null;
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Configuration locations (in order of priority, later overrides earlier).
|
|
81
|
-
*/
|
|
82
|
-
export interface MCPConfigLocations {
|
|
83
|
-
/** User-level config: ~/.pi/mcp.json or ~/.claude.json */
|
|
84
|
-
user?: string;
|
|
85
|
-
/** Project-level config: <cwd>/.mcp.json */
|
|
86
|
-
project?: string;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Get standard MCP config file paths.
|
|
91
|
-
*/
|
|
92
|
-
export function getMCPConfigPaths(cwd: string): MCPConfigLocations {
|
|
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
|
-
|
|
100
|
-
return {
|
|
101
|
-
// User-level: ~/.pi/mcp.json (our standard)
|
|
102
|
-
user: join(home, ".pi", "mcp.json"),
|
|
103
|
-
// Project-level: mcp.json or .mcp.json at project root
|
|
104
|
-
project: projectPath,
|
|
105
|
-
};
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* Merge MCP configs from multiple sources.
|
|
110
|
-
* Later sources override earlier ones for servers with same name.
|
|
111
|
-
*/
|
|
112
|
-
export function mergeMCPConfigs(...configs: (MCPConfigFile | null)[]): Record<string, MCPServerConfig> {
|
|
113
|
-
const result: Record<string, MCPServerConfig> = {};
|
|
114
|
-
|
|
115
|
-
for (const config of configs) {
|
|
116
|
-
if (config?.mcpServers) {
|
|
117
|
-
Object.assign(result, config.mcpServers);
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
return result;
|
|
122
|
-
}
|
|
7
|
+
import { mcpCapability } from "../../capability/mcp";
|
|
8
|
+
import type { MCPServer } from "../../discovery";
|
|
9
|
+
import { load } from "../../discovery";
|
|
10
|
+
import type { MCPServerConfig } from "./types";
|
|
123
11
|
|
|
124
12
|
/** Options for loading MCP configs */
|
|
125
13
|
export interface LoadMCPConfigsOptions {
|
|
126
|
-
/** Additional environment variables for expansion */
|
|
127
|
-
extraEnv?: Record<string, string>;
|
|
128
14
|
/** Whether to load project-level config (default: true) */
|
|
129
15
|
enableProjectConfig?: boolean;
|
|
130
16
|
/** Whether to filter out Exa MCP servers (default: true) */
|
|
@@ -137,43 +23,87 @@ export interface LoadMCPConfigsResult {
|
|
|
137
23
|
configs: Record<string, MCPServerConfig>;
|
|
138
24
|
/** Extracted Exa API keys (if any were filtered) */
|
|
139
25
|
exaApiKeys: string[];
|
|
26
|
+
/** Source metadata for each server */
|
|
27
|
+
sources: Record<string, import("../../capability/types").SourceMeta>;
|
|
140
28
|
}
|
|
141
29
|
|
|
142
30
|
/**
|
|
143
|
-
*
|
|
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
|
|
31
|
+
* Convert canonical MCPServer to legacy MCPServerConfig.
|
|
148
32
|
*/
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
33
|
+
function convertToLegacyConfig(server: MCPServer): MCPServerConfig {
|
|
34
|
+
// Determine transport type
|
|
35
|
+
const transport = server.transport ?? (server.command ? "stdio" : server.url ? "http" : "stdio");
|
|
36
|
+
|
|
37
|
+
if (transport === "stdio") {
|
|
38
|
+
const config: MCPServerConfig = {
|
|
39
|
+
type: "stdio" as const,
|
|
40
|
+
command: server.command ?? "",
|
|
41
|
+
};
|
|
42
|
+
if (server.args) config.args = server.args;
|
|
43
|
+
if (server.env) config.env = server.env;
|
|
44
|
+
return config;
|
|
45
|
+
}
|
|
158
46
|
|
|
159
|
-
|
|
160
|
-
|
|
47
|
+
if (transport === "http") {
|
|
48
|
+
const config: MCPServerConfig = {
|
|
49
|
+
type: "http" as const,
|
|
50
|
+
url: server.url ?? "",
|
|
51
|
+
};
|
|
52
|
+
if (server.headers) config.headers = server.headers;
|
|
53
|
+
return config;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (transport === "sse") {
|
|
57
|
+
const config: MCPServerConfig = {
|
|
58
|
+
type: "sse" as const,
|
|
59
|
+
url: server.url ?? "",
|
|
60
|
+
};
|
|
61
|
+
if (server.headers) config.headers = server.headers;
|
|
62
|
+
return config;
|
|
63
|
+
}
|
|
161
64
|
|
|
162
|
-
|
|
65
|
+
// Fallback to stdio
|
|
66
|
+
return {
|
|
67
|
+
type: "stdio" as const,
|
|
68
|
+
command: server.command ?? "",
|
|
69
|
+
};
|
|
70
|
+
}
|
|
163
71
|
|
|
164
|
-
|
|
165
|
-
|
|
72
|
+
/**
|
|
73
|
+
* Load all MCP server configs from standard locations.
|
|
74
|
+
* Uses the capability system for multi-source discovery.
|
|
75
|
+
*
|
|
76
|
+
* @param cwd Working directory (project root)
|
|
77
|
+
* @param options Load options
|
|
78
|
+
*/
|
|
79
|
+
export async function loadAllMCPConfigs(cwd: string, options?: LoadMCPConfigsOptions): Promise<LoadMCPConfigsResult> {
|
|
80
|
+
const enableProjectConfig = options?.enableProjectConfig ?? true;
|
|
81
|
+
const filterExa = options?.filterExa ?? true;
|
|
82
|
+
|
|
83
|
+
// Load MCP servers via capability system
|
|
84
|
+
const result = await load<MCPServer>(mcpCapability.id, { cwd });
|
|
85
|
+
|
|
86
|
+
// Filter out project-level configs if disabled
|
|
87
|
+
const servers = enableProjectConfig
|
|
88
|
+
? result.items
|
|
89
|
+
: result.items.filter((server) => server._source.level !== "project");
|
|
90
|
+
|
|
91
|
+
// Convert to legacy format and preserve source metadata
|
|
92
|
+
const configs: Record<string, MCPServerConfig> = {};
|
|
93
|
+
const sources: Record<string, import("../../capability/types").SourceMeta> = {};
|
|
94
|
+
for (const server of servers) {
|
|
95
|
+
configs[server.name] = convertToLegacyConfig(server);
|
|
96
|
+
sources[server.name] = server._source;
|
|
97
|
+
}
|
|
166
98
|
|
|
167
|
-
|
|
168
|
-
let exaApiKeys: string[] = [];
|
|
99
|
+
const exaApiKeys: string[] = [];
|
|
169
100
|
|
|
170
101
|
if (filterExa) {
|
|
171
|
-
const
|
|
172
|
-
configs
|
|
173
|
-
exaApiKeys = result.exaApiKeys;
|
|
102
|
+
const filterResult = filterExaMCPServers(configs, sources);
|
|
103
|
+
return { configs: filterResult.configs, exaApiKeys: filterResult.exaApiKeys, sources: filterResult.sources };
|
|
174
104
|
}
|
|
175
105
|
|
|
176
|
-
return { configs, exaApiKeys };
|
|
106
|
+
return { configs, exaApiKeys, sources };
|
|
177
107
|
}
|
|
178
108
|
|
|
179
109
|
/** Pattern to match Exa MCP servers */
|
|
@@ -249,14 +179,20 @@ export interface ExaFilterResult {
|
|
|
249
179
|
configs: Record<string, MCPServerConfig>;
|
|
250
180
|
/** Extracted Exa API keys (if any) */
|
|
251
181
|
exaApiKeys: string[];
|
|
182
|
+
/** Source metadata for remaining servers */
|
|
183
|
+
sources: Record<string, import("../../capability/types").SourceMeta>;
|
|
252
184
|
}
|
|
253
185
|
|
|
254
186
|
/**
|
|
255
187
|
* Filter out Exa MCP servers and extract their API keys.
|
|
256
188
|
* Since we have native Exa integration, we don't need the MCP server.
|
|
257
189
|
*/
|
|
258
|
-
export function filterExaMCPServers(
|
|
190
|
+
export function filterExaMCPServers(
|
|
191
|
+
configs: Record<string, MCPServerConfig>,
|
|
192
|
+
sources: Record<string, import("../../capability/types").SourceMeta>,
|
|
193
|
+
): ExaFilterResult {
|
|
259
194
|
const filtered: Record<string, MCPServerConfig> = {};
|
|
195
|
+
const filteredSources: Record<string, import("../../capability/types").SourceMeta> = {};
|
|
260
196
|
const exaApiKeys: string[] = [];
|
|
261
197
|
|
|
262
198
|
for (const [name, config] of Object.entries(configs)) {
|
|
@@ -268,10 +204,13 @@ export function filterExaMCPServers(configs: Record<string, MCPServerConfig>): E
|
|
|
268
204
|
}
|
|
269
205
|
} else {
|
|
270
206
|
filtered[name] = config;
|
|
207
|
+
if (sources[name]) {
|
|
208
|
+
filteredSources[name] = sources[name];
|
|
209
|
+
}
|
|
271
210
|
}
|
|
272
211
|
}
|
|
273
212
|
|
|
274
|
-
return { configs: filtered, exaApiKeys };
|
|
213
|
+
return { configs: filtered, exaApiKeys, sources: filteredSources };
|
|
275
214
|
}
|
|
276
215
|
|
|
277
216
|
/**
|
|
@@ -282,6 +221,15 @@ export function validateServerConfig(name: string, config: MCPServerConfig): str
|
|
|
282
221
|
|
|
283
222
|
const serverType = config.type ?? "stdio";
|
|
284
223
|
|
|
224
|
+
// Check for conflicting transport fields
|
|
225
|
+
const hasCommand = "command" in config && config.command;
|
|
226
|
+
const hasUrl = "url" in config && (config as { url?: string }).url;
|
|
227
|
+
if (hasCommand && hasUrl) {
|
|
228
|
+
errors.push(
|
|
229
|
+
`Server "${name}": both "command" and "url" are set - server should be either stdio (command) OR http/sse (url), not both`,
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
285
233
|
if (serverType === "stdio") {
|
|
286
234
|
const stdioConfig = config as { command?: string };
|
|
287
235
|
if (!stdioConfig.command) {
|
package/src/core/mcp/index.ts
CHANGED
|
@@ -11,14 +11,10 @@ export { callTool, connectToServer, disconnectServer, listTools, serverSupportsT
|
|
|
11
11
|
// Config
|
|
12
12
|
export type { ExaFilterResult, LoadMCPConfigsOptions, LoadMCPConfigsResult } from "./config";
|
|
13
13
|
export {
|
|
14
|
-
expandEnvVars,
|
|
15
14
|
extractExaApiKey,
|
|
16
15
|
filterExaMCPServers,
|
|
17
|
-
getMCPConfigPaths,
|
|
18
16
|
isExaMCPServer,
|
|
19
17
|
loadAllMCPConfigs,
|
|
20
|
-
loadMCPConfigFile,
|
|
21
|
-
mergeMCPConfigs,
|
|
22
18
|
validateServerConfig,
|
|
23
19
|
} from "./config";
|
|
24
20
|
// Loader (for SDK integration)
|
package/src/core/mcp/loader.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import type { LoadedCustomTool } from "../custom-tools/types";
|
|
8
8
|
import { type MCPLoadResult, MCPManager } from "./manager";
|
|
9
|
+
import { parseMCPToolName } from "./tool-bridge";
|
|
9
10
|
|
|
10
11
|
/** Result from loading MCP tools */
|
|
11
12
|
export interface MCPToolsLoadResult {
|
|
@@ -23,8 +24,6 @@ export interface MCPToolsLoadResult {
|
|
|
23
24
|
|
|
24
25
|
/** Options for loading MCP tools */
|
|
25
26
|
export interface MCPToolsLoadOptions {
|
|
26
|
-
/** Additional environment variables for expansion */
|
|
27
|
-
extraEnv?: Record<string, string>;
|
|
28
27
|
/** Called when starting to connect to servers */
|
|
29
28
|
onConnecting?: (serverNames: string[]) => void;
|
|
30
29
|
/** Whether to load project-level config (default: true) */
|
|
@@ -37,28 +36,18 @@ export interface MCPToolsLoadOptions {
|
|
|
37
36
|
* Discover and load MCP tools from .mcp.json files.
|
|
38
37
|
*
|
|
39
38
|
* @param cwd Working directory (project root)
|
|
40
|
-
* @param options Load options including
|
|
39
|
+
* @param options Load options including progress callbacks
|
|
41
40
|
* @returns MCP tools in LoadedCustomTool format for integration
|
|
42
41
|
*/
|
|
43
|
-
export async function discoverAndLoadMCPTools(
|
|
44
|
-
cwd: string,
|
|
45
|
-
options?: MCPToolsLoadOptions | Record<string, string>,
|
|
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
|
-
|
|
42
|
+
export async function discoverAndLoadMCPTools(cwd: string, options?: MCPToolsLoadOptions): Promise<MCPToolsLoadResult> {
|
|
53
43
|
const manager = new MCPManager(cwd);
|
|
54
44
|
|
|
55
45
|
let result: MCPLoadResult;
|
|
56
46
|
try {
|
|
57
47
|
result = await manager.discoverAndConnect({
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
filterExa: opts.filterExa,
|
|
48
|
+
onConnecting: options?.onConnecting,
|
|
49
|
+
enableProjectConfig: options?.enableProjectConfig,
|
|
50
|
+
filterExa: options?.filterExa,
|
|
62
51
|
});
|
|
63
52
|
} catch (error) {
|
|
64
53
|
// If discovery fails entirely, return empty result
|
|
@@ -73,11 +62,26 @@ export async function discoverAndLoadMCPTools(
|
|
|
73
62
|
}
|
|
74
63
|
|
|
75
64
|
// Convert MCP tools to LoadedCustomTool format
|
|
76
|
-
const loadedTools: LoadedCustomTool[] = result.tools.map((tool) =>
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
65
|
+
const loadedTools: LoadedCustomTool[] = result.tools.map((tool) => {
|
|
66
|
+
// Parse the MCP tool name to get server name
|
|
67
|
+
const parsed = parseMCPToolName(tool.name);
|
|
68
|
+
const serverName = parsed?.serverName;
|
|
69
|
+
|
|
70
|
+
// Get provider info from manager's connection if available
|
|
71
|
+
const connection = serverName ? manager.getConnection(serverName) : undefined;
|
|
72
|
+
const provider = connection?._source?.provider;
|
|
73
|
+
|
|
74
|
+
// Format path with provider info if available
|
|
75
|
+
// Format: "mcp:serverName via providerName" (e.g., "mcp:agentx via Claude Code")
|
|
76
|
+
const path =
|
|
77
|
+
provider && serverName ? `mcp:${serverName} via ${connection._source!.providerName}` : `mcp:${tool.name}`;
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
path,
|
|
81
|
+
resolvedPath: `mcp:${tool.name}`,
|
|
82
|
+
tool: tool as any, // MCPToolDetails is compatible with CustomTool<TSchema, any>
|
|
83
|
+
};
|
|
84
|
+
});
|
|
81
85
|
|
|
82
86
|
// Convert error map to array format
|
|
83
87
|
const errors: Array<{ path: string; error: string }> = [];
|
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";
|
|
10
10
|
import { connectToServer, disconnectServer, listTools } from "./client";
|
|
11
|
-
import {
|
|
11
|
+
import { loadAllMCPConfigs, validateServerConfig } from "./config";
|
|
12
12
|
import type { MCPToolDetails } from "./tool-bridge";
|
|
13
13
|
import { createMCPTools } from "./tool-bridge";
|
|
14
14
|
import type { MCPServerConfig, MCPServerConnection } from "./types";
|
|
@@ -26,7 +26,11 @@ export interface MCPLoadResult {
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
/** Options for discovering and connecting to MCP servers */
|
|
29
|
-
export interface MCPDiscoverOptions
|
|
29
|
+
export interface MCPDiscoverOptions {
|
|
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;
|
|
30
34
|
/** Called when starting to connect to servers */
|
|
31
35
|
onConnecting?: (serverNames: string[]) => void;
|
|
32
36
|
}
|
|
@@ -46,26 +50,12 @@ export class MCPManager {
|
|
|
46
50
|
* Discover and connect to all MCP servers from .mcp.json files.
|
|
47
51
|
* Returns tools and any connection errors.
|
|
48
52
|
*/
|
|
49
|
-
async discoverAndConnect(
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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,
|
|
53
|
+
async discoverAndConnect(options?: MCPDiscoverOptions): Promise<MCPLoadResult> {
|
|
54
|
+
const { configs, exaApiKeys, sources } = await loadAllMCPConfigs(this.cwd, {
|
|
55
|
+
enableProjectConfig: options?.enableProjectConfig,
|
|
56
|
+
filterExa: options?.filterExa,
|
|
67
57
|
});
|
|
68
|
-
const result = await this.connectServers(configs,
|
|
58
|
+
const result = await this.connectServers(configs, sources, options?.onConnecting);
|
|
69
59
|
result.exaApiKeys = exaApiKeys;
|
|
70
60
|
return result;
|
|
71
61
|
}
|
|
@@ -76,6 +66,7 @@ export class MCPManager {
|
|
|
76
66
|
*/
|
|
77
67
|
async connectServers(
|
|
78
68
|
configs: Record<string, MCPServerConfig>,
|
|
69
|
+
sources: Record<string, import("../../capability/types").SourceMeta>,
|
|
79
70
|
onConnecting?: (serverNames: string[]) => void,
|
|
80
71
|
): Promise<MCPLoadResult> {
|
|
81
72
|
const errors = new Map<string, string>();
|
|
@@ -115,6 +106,10 @@ export class MCPManager {
|
|
|
115
106
|
const results = await Promise.allSettled(
|
|
116
107
|
connectionTasks.map(async ({ name, config }) => {
|
|
117
108
|
const connection = await connectToServer(name, config);
|
|
109
|
+
// Attach source metadata to connection
|
|
110
|
+
if (sources[name]) {
|
|
111
|
+
connection._source = sources[name];
|
|
112
|
+
}
|
|
118
113
|
const serverTools = await listTools(connection);
|
|
119
114
|
return { name, connection, serverTools };
|
|
120
115
|
}),
|
|
@@ -229,12 +224,12 @@ export class MCPManager {
|
|
|
229
224
|
*/
|
|
230
225
|
export async function createMCPManager(
|
|
231
226
|
cwd: string,
|
|
232
|
-
|
|
227
|
+
options?: MCPDiscoverOptions,
|
|
233
228
|
): Promise<{
|
|
234
229
|
manager: MCPManager;
|
|
235
230
|
result: MCPLoadResult;
|
|
236
231
|
}> {
|
|
237
232
|
const manager = new MCPManager(cwd);
|
|
238
|
-
const result = await manager.discoverAndConnect(
|
|
233
|
+
const result = await manager.discoverAndConnect(options);
|
|
239
234
|
return { manager, result };
|
|
240
235
|
}
|
|
@@ -19,6 +19,10 @@ export interface MCPToolDetails {
|
|
|
19
19
|
isError?: boolean;
|
|
20
20
|
/** Raw content from MCP response */
|
|
21
21
|
rawContent?: MCPContent[];
|
|
22
|
+
/** Provider ID (e.g., "claude", "mcp-json") */
|
|
23
|
+
provider?: string;
|
|
24
|
+
/** Provider display name (e.g., "Claude Code", "MCP Config") */
|
|
25
|
+
providerName?: string;
|
|
22
26
|
}
|
|
23
27
|
|
|
24
28
|
/**
|
|
@@ -74,7 +78,7 @@ export function parseMCPToolName(name: string): { serverName: string; toolName:
|
|
|
74
78
|
if (!name.startsWith("mcp_")) return null;
|
|
75
79
|
|
|
76
80
|
const rest = name.slice(4);
|
|
77
|
-
const underscoreIdx = rest.
|
|
81
|
+
const underscoreIdx = rest.lastIndexOf("_");
|
|
78
82
|
if (underscoreIdx === -1) return null;
|
|
79
83
|
|
|
80
84
|
return {
|
|
@@ -109,6 +113,8 @@ export function createMCPTool(
|
|
|
109
113
|
mcpToolName: tool.name,
|
|
110
114
|
isError: result.isError,
|
|
111
115
|
rawContent: result.content,
|
|
116
|
+
provider: connection._source?.provider,
|
|
117
|
+
providerName: connection._source?.providerName,
|
|
112
118
|
};
|
|
113
119
|
|
|
114
120
|
if (result.isError) {
|
|
@@ -130,6 +136,8 @@ export function createMCPTool(
|
|
|
130
136
|
serverName: connection.name,
|
|
131
137
|
mcpToolName: tool.name,
|
|
132
138
|
isError: true,
|
|
139
|
+
provider: connection._source?.provider,
|
|
140
|
+
providerName: connection._source?.providerName,
|
|
133
141
|
},
|
|
134
142
|
};
|
|
135
143
|
}
|
package/src/core/mcp/types.ts
CHANGED
|
@@ -217,6 +217,8 @@ export interface MCPServerConnection {
|
|
|
217
217
|
capabilities: MCPServerCapabilities;
|
|
218
218
|
/** Cached tools (populated on demand) */
|
|
219
219
|
tools?: MCPToolDefinition[];
|
|
220
|
+
/** Source metadata (for display) */
|
|
221
|
+
_source?: import("../../capability/types").SourceMeta;
|
|
220
222
|
}
|
|
221
223
|
|
|
222
224
|
/** MCP tool with server context */
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
import { type Static, Type } from "@sinclair/typebox";
|
|
16
16
|
import AjvModule from "ajv";
|
|
17
17
|
import type { AuthStorage } from "./auth-storage";
|
|
18
|
+
import { logger } from "./logger";
|
|
18
19
|
|
|
19
20
|
const Ajv = (AjvModule as any).default || AjvModule;
|
|
20
21
|
|
|
@@ -92,9 +93,15 @@ export class ModelRegistry {
|
|
|
92
93
|
private customProviderApiKeys: Map<string, string> = new Map();
|
|
93
94
|
private loadError: string | undefined = undefined;
|
|
94
95
|
|
|
96
|
+
/**
|
|
97
|
+
* @param authStorage - Auth storage for API key resolution
|
|
98
|
+
* @param modelsJsonPath - Primary path for models.json
|
|
99
|
+
* @param fallbackPaths - Additional paths to check (legacy support)
|
|
100
|
+
*/
|
|
95
101
|
constructor(
|
|
96
102
|
readonly authStorage: AuthStorage,
|
|
97
103
|
private modelsJsonPath: string | undefined = undefined,
|
|
104
|
+
private fallbackPaths: string[] = [],
|
|
98
105
|
) {
|
|
99
106
|
// Set up fallback resolver for custom provider API keys
|
|
100
107
|
this.authStorage.setFallbackResolver((provider) => {
|
|
@@ -133,15 +140,25 @@ export class ModelRegistry {
|
|
|
133
140
|
builtInModels.push(...(providerModels as Model<Api>[]));
|
|
134
141
|
}
|
|
135
142
|
|
|
136
|
-
// Load custom models from models.json (
|
|
143
|
+
// Load custom models from models.json (check primary path, then fallbacks)
|
|
137
144
|
let customModels: Model<Api>[] = [];
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
+
const pathsToCheck = this.modelsJsonPath ? [this.modelsJsonPath, ...this.fallbackPaths] : this.fallbackPaths;
|
|
146
|
+
|
|
147
|
+
if (pathsToCheck.length > 0) {
|
|
148
|
+
logger.debug("ModelRegistry.loadModels checking paths", { paths: pathsToCheck });
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
for (const modelsPath of pathsToCheck) {
|
|
152
|
+
if (existsSync(modelsPath)) {
|
|
153
|
+
logger.debug("ModelRegistry.loadModels loading", { path: modelsPath });
|
|
154
|
+
const result = this.loadCustomModels(modelsPath);
|
|
155
|
+
if (result.error) {
|
|
156
|
+
this.loadError = result.error;
|
|
157
|
+
// Keep built-in models even if custom models failed to load
|
|
158
|
+
} else {
|
|
159
|
+
customModels = result.models;
|
|
160
|
+
}
|
|
161
|
+
break; // Use first existing file
|
|
145
162
|
}
|
|
146
163
|
}
|
|
147
164
|
|
|
@@ -39,7 +39,7 @@ export async function installPlugin(packageName: string): Promise<InstalledPlugi
|
|
|
39
39
|
// Initialize package.json if it doesn't exist
|
|
40
40
|
const pkgJsonPath = join(PLUGINS_DIR, "package.json");
|
|
41
41
|
if (!(await Bun.file(pkgJsonPath).exists())) {
|
|
42
|
-
await Bun.write(pkgJsonPath, JSON.stringify({ name: "
|
|
42
|
+
await Bun.write(pkgJsonPath, JSON.stringify({ name: "omp-plugins", private: true, dependencies: {} }, null, 2));
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
// Run npm install in plugins directory
|
|
@@ -7,7 +7,12 @@
|
|
|
7
7
|
|
|
8
8
|
import { existsSync, readFileSync } from "node:fs";
|
|
9
9
|
import { join } from "node:path";
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
getAllProjectPluginOverridePaths,
|
|
12
|
+
getPluginsLockfile,
|
|
13
|
+
getPluginsNodeModules,
|
|
14
|
+
getPluginsPackageJson,
|
|
15
|
+
} from "./paths";
|
|
11
16
|
import type { InstalledPlugin, PluginManifest, PluginRuntimeConfig, ProjectPluginOverrides } from "./types";
|
|
12
17
|
|
|
13
18
|
// =============================================================================
|
|
@@ -30,18 +35,19 @@ function loadRuntimeConfig(): PluginRuntimeConfig {
|
|
|
30
35
|
}
|
|
31
36
|
|
|
32
37
|
/**
|
|
33
|
-
* Load project-local plugin overrides.
|
|
38
|
+
* Load project-local plugin overrides (checks .omp and .pi directories).
|
|
34
39
|
*/
|
|
35
40
|
function loadProjectOverrides(cwd: string): ProjectPluginOverrides {
|
|
36
|
-
const overridesPath
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
for (const overridesPath of getAllProjectPluginOverridePaths(cwd)) {
|
|
42
|
+
if (existsSync(overridesPath)) {
|
|
43
|
+
try {
|
|
44
|
+
return JSON.parse(readFileSync(overridesPath, "utf-8"));
|
|
45
|
+
} catch {
|
|
46
|
+
// Continue to next path
|
|
47
|
+
}
|
|
48
|
+
}
|
|
44
49
|
}
|
|
50
|
+
return {};
|
|
45
51
|
}
|
|
46
52
|
|
|
47
53
|
// =============================================================================
|
|
@@ -79,7 +85,7 @@ export function getEnabledPlugins(cwd: string): InstalledPlugin[] {
|
|
|
79
85
|
const manifest: PluginManifest | undefined = pluginPkg.omp || pluginPkg.pi;
|
|
80
86
|
|
|
81
87
|
if (!manifest) {
|
|
82
|
-
// Not
|
|
88
|
+
// Not an omp plugin, skip
|
|
83
89
|
continue;
|
|
84
90
|
}
|
|
85
91
|
|
|
@@ -110,7 +110,7 @@ export class PluginManager {
|
|
|
110
110
|
pkgJsonPath,
|
|
111
111
|
JSON.stringify(
|
|
112
112
|
{
|
|
113
|
-
name: "
|
|
113
|
+
name: "omp-plugins",
|
|
114
114
|
private: true,
|
|
115
115
|
dependencies: {},
|
|
116
116
|
},
|
|
@@ -516,7 +516,7 @@ export class PluginManager {
|
|
|
516
516
|
status: hasManifest ? "ok" : "warning",
|
|
517
517
|
message: hasManifest
|
|
518
518
|
? `v${pluginPkg.version}${pluginPkg.description ? ` - ${pluginPkg.description}` : ""}`
|
|
519
|
-
: `v${pluginPkg.version} - No omp/pi manifest (not
|
|
519
|
+
: `v${pluginPkg.version} - No omp/pi manifest (not an omp plugin)`,
|
|
520
520
|
});
|
|
521
521
|
|
|
522
522
|
// Check tools path exists if specified
|