@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.
Files changed (117) hide show
  1. package/CHANGELOG.md +72 -34
  2. package/README.md +100 -100
  3. package/docs/compaction.md +8 -8
  4. package/docs/config-usage.md +113 -0
  5. package/docs/custom-tools.md +8 -8
  6. package/docs/extension-loading.md +58 -58
  7. package/docs/hooks.md +11 -11
  8. package/docs/rpc.md +4 -4
  9. package/docs/sdk.md +14 -14
  10. package/docs/session-tree-plan.md +1 -1
  11. package/docs/session.md +2 -2
  12. package/docs/skills.md +16 -16
  13. package/docs/theme.md +9 -9
  14. package/docs/tui.md +1 -1
  15. package/examples/README.md +1 -1
  16. package/examples/custom-tools/README.md +4 -4
  17. package/examples/custom-tools/subagent/README.md +13 -13
  18. package/examples/custom-tools/subagent/agents.ts +2 -2
  19. package/examples/custom-tools/subagent/index.ts +5 -5
  20. package/examples/hooks/README.md +3 -3
  21. package/examples/hooks/auto-commit-on-exit.ts +1 -1
  22. package/examples/hooks/custom-compaction.ts +1 -1
  23. package/examples/sdk/01-minimal.ts +1 -1
  24. package/examples/sdk/04-skills.ts +1 -1
  25. package/examples/sdk/05-tools.ts +1 -1
  26. package/examples/sdk/08-slash-commands.ts +1 -1
  27. package/examples/sdk/09-api-keys-and-oauth.ts +2 -2
  28. package/examples/sdk/README.md +2 -2
  29. package/package.json +13 -11
  30. package/src/capability/context-file.ts +40 -0
  31. package/src/capability/extension.ts +48 -0
  32. package/src/capability/hook.ts +40 -0
  33. package/src/capability/index.ts +616 -0
  34. package/src/capability/instruction.ts +37 -0
  35. package/src/capability/mcp.ts +52 -0
  36. package/src/capability/prompt.ts +35 -0
  37. package/src/capability/rule.ts +52 -0
  38. package/src/capability/settings.ts +35 -0
  39. package/src/capability/skill.ts +49 -0
  40. package/src/capability/slash-command.ts +40 -0
  41. package/src/capability/system-prompt.ts +35 -0
  42. package/src/capability/tool.ts +38 -0
  43. package/src/capability/types.ts +166 -0
  44. package/src/cli/args.ts +2 -2
  45. package/src/cli/plugin-cli.ts +24 -19
  46. package/src/cli/update-cli.ts +10 -10
  47. package/src/config.ts +290 -6
  48. package/src/core/auth-storage.ts +32 -9
  49. package/src/core/bash-executor.ts +1 -1
  50. package/src/core/custom-commands/loader.ts +44 -50
  51. package/src/core/custom-tools/index.ts +1 -0
  52. package/src/core/custom-tools/loader.ts +67 -69
  53. package/src/core/custom-tools/types.ts +10 -1
  54. package/src/core/hooks/loader.ts +13 -42
  55. package/src/core/index.ts +0 -1
  56. package/src/core/logger.ts +7 -7
  57. package/src/core/mcp/client.ts +1 -1
  58. package/src/core/mcp/config.ts +94 -146
  59. package/src/core/mcp/index.ts +0 -4
  60. package/src/core/mcp/loader.ts +26 -22
  61. package/src/core/mcp/manager.ts +18 -23
  62. package/src/core/mcp/tool-bridge.ts +9 -1
  63. package/src/core/mcp/types.ts +2 -0
  64. package/src/core/model-registry.ts +25 -8
  65. package/src/core/plugins/installer.ts +1 -1
  66. package/src/core/plugins/loader.ts +17 -11
  67. package/src/core/plugins/manager.ts +2 -2
  68. package/src/core/plugins/paths.ts +12 -7
  69. package/src/core/plugins/types.ts +3 -3
  70. package/src/core/sdk.ts +48 -27
  71. package/src/core/session-manager.ts +4 -4
  72. package/src/core/settings-manager.ts +45 -21
  73. package/src/core/skills.ts +208 -293
  74. package/src/core/slash-commands.ts +34 -165
  75. package/src/core/system-prompt.ts +58 -65
  76. package/src/core/timings.ts +2 -2
  77. package/src/core/tools/lsp/config.ts +38 -17
  78. package/src/core/tools/task/agents.ts +21 -0
  79. package/src/core/tools/task/artifacts.ts +1 -1
  80. package/src/core/tools/task/bundled-agents/reviewer.md +2 -1
  81. package/src/core/tools/task/bundled-agents/task.md +1 -0
  82. package/src/core/tools/task/commands.ts +30 -107
  83. package/src/core/tools/task/discovery.ts +75 -66
  84. package/src/core/tools/task/executor.ts +25 -10
  85. package/src/core/tools/task/index.ts +35 -10
  86. package/src/core/tools/task/model-resolver.ts +27 -25
  87. package/src/core/tools/task/types.ts +6 -2
  88. package/src/core/tools/web-fetch.ts +3 -3
  89. package/src/core/tools/web-search/auth.ts +40 -34
  90. package/src/core/tools/web-search/index.ts +1 -1
  91. package/src/core/tools/web-search/providers/anthropic.ts +1 -1
  92. package/src/discovery/agents-md.ts +75 -0
  93. package/src/discovery/builtin.ts +646 -0
  94. package/src/discovery/claude.ts +623 -0
  95. package/src/discovery/cline.ts +102 -0
  96. package/src/discovery/codex.ts +571 -0
  97. package/src/discovery/cursor.ts +264 -0
  98. package/src/discovery/gemini.ts +368 -0
  99. package/src/discovery/github.ts +120 -0
  100. package/src/discovery/helpers.test.ts +127 -0
  101. package/src/discovery/helpers.ts +249 -0
  102. package/src/discovery/index.ts +84 -0
  103. package/src/discovery/mcp-json.ts +127 -0
  104. package/src/discovery/vscode.ts +99 -0
  105. package/src/discovery/windsurf.ts +216 -0
  106. package/src/main.ts +14 -13
  107. package/src/migrations.ts +24 -3
  108. package/src/modes/interactive/components/hook-editor.ts +1 -1
  109. package/src/modes/interactive/components/plugin-settings.ts +1 -1
  110. package/src/modes/interactive/components/settings-defs.ts +38 -2
  111. package/src/modes/interactive/components/settings-selector.ts +1 -0
  112. package/src/modes/interactive/components/welcome.ts +2 -2
  113. package/src/modes/interactive/interactive-mode.ts +233 -16
  114. package/src/modes/interactive/theme/theme-schema.json +1 -1
  115. package/src/utils/clipboard.ts +1 -1
  116. package/src/utils/shell-snapshot.ts +2 -2
  117. package/src/utils/shell.ts +7 -7
@@ -1,130 +1,16 @@
1
1
  /**
2
2
  * MCP configuration loader.
3
3
  *
4
- * Loads .mcp.json files from project root with environment variable expansion.
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 { existsSync, readFileSync } from "node:fs";
9
- import { homedir } from "node:os";
10
- import { join } from "node:path";
11
- import type { MCPConfigFile, MCPServerConfig } from "./types";
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
- * Load all MCP server configs from standard locations.
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
- 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 };
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
- const enableProjectConfig = opts.enableProjectConfig ?? true;
160
- const filterExa = opts.filterExa ?? true;
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
- const paths = getMCPConfigPaths(cwd);
65
+ // Fallback to stdio
66
+ return {
67
+ type: "stdio" as const,
68
+ command: server.command ?? "",
69
+ };
70
+ }
163
71
 
164
- const userConfig = paths.user ? loadMCPConfigFile(paths.user, opts.extraEnv) : null;
165
- const projectConfig = enableProjectConfig && paths.project ? loadMCPConfigFile(paths.project, opts.extraEnv) : null;
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
- let configs = mergeMCPConfigs(userConfig, projectConfig);
168
- let exaApiKeys: string[] = [];
99
+ const exaApiKeys: string[] = [];
169
100
 
170
101
  if (filterExa) {
171
- const result = filterExaMCPServers(configs);
172
- configs = result.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(configs: Record<string, MCPServerConfig>): ExaFilterResult {
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) {
@@ -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)
@@ -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 extraEnv and progress callbacks
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
- extraEnv: opts.extraEnv,
59
- onConnecting: opts.onConnecting,
60
- enableProjectConfig: opts.enableProjectConfig,
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
- path: `mcp:${tool.name}`,
78
- resolvedPath: `mcp:${tool.name}`,
79
- tool: tool as any, // MCPToolDetails is compatible with CustomTool<TSchema, any>
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 }> = [];
@@ -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 { type LoadMCPConfigsOptions, loadAllMCPConfigs, validateServerConfig } from "./config";
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 extends LoadMCPConfigsOptions {
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
- 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,
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, opts.onConnecting);
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
- extraEnv?: Record<string, string>,
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(extraEnv);
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.indexOf("_");
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
  }
@@ -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 (if path provided)
143
+ // Load custom models from models.json (check primary path, then fallbacks)
137
144
  let customModels: Model<Api>[] = [];
138
- if (this.modelsJsonPath) {
139
- const result = this.loadCustomModels(this.modelsJsonPath);
140
- if (result.error) {
141
- this.loadError = result.error;
142
- // Keep built-in models even if custom models failed to load
143
- } else {
144
- customModels = result.models;
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: "pi-plugins", private: true, dependencies: {} }, null, 2));
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 { getPluginsLockfile, getPluginsNodeModules, getPluginsPackageJson, getProjectPluginOverrides } from "./paths";
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 = getProjectPluginOverrides(cwd);
37
- if (!existsSync(overridesPath)) {
38
- return {};
39
- }
40
- try {
41
- return JSON.parse(readFileSync(overridesPath, "utf-8"));
42
- } catch {
43
- return {};
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 a pi plugin, skip
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: "pi-plugins",
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 a pi plugin)`,
519
+ : `v${pluginPkg.version} - No omp/pi manifest (not an omp plugin)`,
520
520
  });
521
521
 
522
522
  // Check tools path exists if specified