@oh-my-pi/pi-coding-agent 11.8.3 → 11.9.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 CHANGED
@@ -2,6 +2,48 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [11.9.0] - 2026-02-10
6
+
7
+ ### Added
8
+
9
+ - Added `/mcp` slash command for runtime MCP server management (add, list, remove, enable, disable, test, reauth)
10
+ - Added interactive multi-step wizard for adding MCP servers with transport auto-detection
11
+ - Added OAuth auto-discovery and authentication flow for MCP servers requiring authorization
12
+ - Added MCP config file writer for persisting server configurations at user and project level
13
+ - Added `enabled` and `timeout` fields to MCP server configuration
14
+ - Added runtime MCP manager reload and active tool registry rebind without restart
15
+ - Added MCP command guide documentation
16
+
17
+ ### Changed
18
+
19
+ - Replaced `setTimeout` with `Bun.sleep()` for improved performance in file lock retry logic
20
+ - Refactored component invalidation handling to use dedicated helper function for cleaner code
21
+ - Improved error handling in worktree baseline application to use `isEnoent()` utility instead of file existence checks
22
+ - Updated bash tool to use standard Node.js `fs.promises.stat()` with `isEnoent()` error handling
23
+ - Replaced `tmpdir()` named import with `os` namespace import for consistency
24
+ - Migrated logging from `chalk` and `console.error` to structured logger from `@oh-my-pi/pi-utils`
25
+
26
+ ### Fixed
27
+
28
+ - Improved browser script evaluation to handle both expression and statement forms, fixing evaluation failures for certain script types
29
+ - Fixed unsafe OAuth endpoint extraction that could redirect token exchange to attacker-controlled URLs
30
+ - Fixed PKCE code verifier stored via untyped property; now uses typed private field
31
+ - Fixed refresh token fallback incorrectly using access token when no refresh token provided
32
+ - Fixed MCP config files written with default permissions; now enforces 0o700/0o600 for secret protection
33
+ - Fixed add wizard ignoring user-chosen environment variable name and auth header name
34
+ - Fixed reauth endpoint discovery misclassifying non-OAuth servers as discovery failures
35
+ - Fixed resolved OAuth tokens leaking into connection config, causing cache churn on token rotation
36
+ - Fixed unvalidated type assertions for `enabled`/`timeout` config fields from user-controlled JSON
37
+ - Fixed uncaught exceptions in `/mcp add` quick-add flow crashing the interactive loop
38
+ - Fixed greedy `/mcp` prefix match routing `/mcpfoo` to MCP controller
39
+ - Fixed stdio transport timeout timer leak keeping process alive after request completion
40
+
41
+ ### Removed
42
+
43
+ - Removed `GrepOperations` interface from public API exports
44
+ - Removed `GrepToolOptions` interface from public API exports
45
+ - Removed unused `_options` parameter from `GrepTool` constructor
46
+
5
47
  ## [11.8.1] - 2026-02-10
6
48
 
7
49
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oh-my-pi/pi-coding-agent",
3
- "version": "11.8.3",
3
+ "version": "11.9.0",
4
4
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
5
5
  "type": "module",
6
6
  "ompConfig": {
@@ -90,12 +90,12 @@
90
90
  "@mozilla/readability": "0.6.0",
91
91
  "@oclif/core": "^4.8.0",
92
92
  "@oclif/plugin-autocomplete": "^3.2.40",
93
- "@oh-my-pi/omp-stats": "11.8.3",
94
- "@oh-my-pi/pi-agent-core": "11.8.3",
95
- "@oh-my-pi/pi-ai": "11.8.3",
96
- "@oh-my-pi/pi-natives": "11.8.3",
97
- "@oh-my-pi/pi-tui": "11.8.3",
98
- "@oh-my-pi/pi-utils": "11.8.3",
93
+ "@oh-my-pi/omp-stats": "11.9.0",
94
+ "@oh-my-pi/pi-agent-core": "11.9.0",
95
+ "@oh-my-pi/pi-ai": "11.9.0",
96
+ "@oh-my-pi/pi-natives": "11.9.0",
97
+ "@oh-my-pi/pi-tui": "11.9.0",
98
+ "@oh-my-pi/pi-utils": "11.9.0",
99
99
  "@sinclair/typebox": "^0.34.48",
100
100
  "ajv": "^8.17.1",
101
101
  "chalk": "^5.6.2",
@@ -13,6 +13,10 @@ import type { SourceMeta } from "./types";
13
13
  export interface MCPServer {
14
14
  /** Server name (unique key) */
15
15
  name: string;
16
+ /** Whether this server is enabled (default: true) */
17
+ enabled?: boolean;
18
+ /** Connection timeout in milliseconds */
19
+ timeout?: number;
16
20
  /** Command to run (for stdio transport) */
17
21
  command?: string;
18
22
  /** Command arguments */
@@ -23,6 +27,11 @@ export interface MCPServer {
23
27
  url?: string;
24
28
  /** HTTP headers (for HTTP transport) */
25
29
  headers?: Record<string, string>;
30
+ /** Authentication configuration */
31
+ auth?: {
32
+ type: "oauth" | "apikey";
33
+ credentialId?: string;
34
+ };
26
35
  /** Transport type */
27
36
  transport?: "stdio" | "sse" | "http";
28
37
  /** Source metadata (added by loader) */
@@ -101,7 +101,7 @@ async function acquireLock(filePath: string, options: FileLockOptions = {}): Pro
101
101
  continue;
102
102
  }
103
103
 
104
- await new Promise(resolve => setTimeout(resolve, opts.retryDelayMs));
104
+ await Bun.sleep(opts.retryDelayMs);
105
105
  }
106
106
 
107
107
  throw new Error(`Failed to acquire lock for ${filePath} after ${opts.retries} attempts`);
@@ -5,6 +5,7 @@
5
5
  * .pi is an alias for backwards compatibility.
6
6
  */
7
7
  import * as path from "node:path";
8
+ import { logger } from "@oh-my-pi/pi-utils";
8
9
  import { registerProvider } from "../capability";
9
10
  import { type ContextFile, contextFileCapability } from "../capability/context-file";
10
11
  import { type Extension, type ExtensionManifest, extensionCapability } from "../capability/extension";
@@ -79,13 +80,60 @@ async function loadMCPServers(ctx: LoadContext): Promise<LoadResult<MCPServer>>
79
80
  const expanded = expandEnvVarsDeep(data.mcpServers);
80
81
  for (const [serverName, config] of Object.entries(expanded)) {
81
82
  const serverConfig = config as Record<string, unknown>;
83
+
84
+ // Validate enabled: coerce string "true"/"false", warn on other types
85
+ let enabled: boolean | undefined;
86
+ if (serverConfig.enabled === undefined || serverConfig.enabled === null) {
87
+ enabled = undefined;
88
+ } else if (typeof serverConfig.enabled === "boolean") {
89
+ enabled = serverConfig.enabled;
90
+ } else if (typeof serverConfig.enabled === "string") {
91
+ const lower = serverConfig.enabled.toLowerCase();
92
+ if (lower === "false" || lower === "0") enabled = false;
93
+ else if (lower === "true" || lower === "1") enabled = true;
94
+ else {
95
+ logger.warn(`MCP server "${serverName}": invalid enabled value "${serverConfig.enabled}", ignoring`);
96
+ enabled = undefined;
97
+ }
98
+ } else {
99
+ logger.warn(`MCP server "${serverName}": invalid enabled type ${typeof serverConfig.enabled}, ignoring`);
100
+ enabled = undefined;
101
+ }
102
+
103
+ // Validate timeout: coerce numeric strings, warn on invalid
104
+ let timeout: number | undefined;
105
+ if (serverConfig.timeout === undefined || serverConfig.timeout === null) {
106
+ timeout = undefined;
107
+ } else if (typeof serverConfig.timeout === "number") {
108
+ if (Number.isFinite(serverConfig.timeout) && serverConfig.timeout > 0) {
109
+ timeout = serverConfig.timeout;
110
+ } else {
111
+ logger.warn(`MCP server "${serverName}": invalid timeout ${serverConfig.timeout}, ignoring`);
112
+ timeout = undefined;
113
+ }
114
+ } else if (typeof serverConfig.timeout === "string") {
115
+ const parsed = Number(serverConfig.timeout);
116
+ if (Number.isFinite(parsed) && parsed > 0) {
117
+ timeout = parsed;
118
+ } else {
119
+ logger.warn(`MCP server "${serverName}": invalid timeout "${serverConfig.timeout}", ignoring`);
120
+ timeout = undefined;
121
+ }
122
+ } else {
123
+ logger.warn(`MCP server "${serverName}": invalid timeout type ${typeof serverConfig.timeout}, ignoring`);
124
+ timeout = undefined;
125
+ }
126
+
82
127
  result.push({
83
128
  name: serverName,
129
+ enabled,
130
+ timeout,
84
131
  command: serverConfig.command as string | undefined,
85
132
  args: serverConfig.args as string[] | undefined,
86
133
  env: serverConfig.env as Record<string, string> | undefined,
87
134
  url: serverConfig.url as string | undefined,
88
135
  headers: serverConfig.headers as Record<string, string> | undefined,
136
+ auth: serverConfig.auth as { type: "oauth" | "apikey"; credentialId?: string } | undefined,
89
137
  transport: serverConfig.type as "stdio" | "sse" | "http" | undefined,
90
138
  _source: createSourceMeta(PROVIDER_ID, path, level),
91
139
  });
@@ -7,6 +7,7 @@
7
7
  * Priority: 5 (low, as this is a fallback after tool-specific providers)
8
8
  */
9
9
  import * as path from "node:path";
10
+ import { logger } from "@oh-my-pi/pi-utils";
10
11
  import { registerProvider } from "../capability";
11
12
  import { readFile } from "../capability/fs";
12
13
  import { type MCPServer, mcpCapability } from "../capability/mcp";
@@ -23,11 +24,17 @@ interface MCPConfigFile {
23
24
  mcpServers?: Record<
24
25
  string,
25
26
  {
27
+ enabled?: boolean;
28
+ timeout?: number;
26
29
  command?: string;
27
30
  args?: string[];
28
31
  env?: Record<string, string>;
29
32
  url?: string;
30
33
  headers?: Record<string, string>;
34
+ auth?: {
35
+ type: "oauth" | "apikey";
36
+ credentialId?: string;
37
+ };
31
38
  type?: "stdio" | "sse" | "http";
32
39
  }
33
40
  >;
@@ -41,13 +48,39 @@ function transformMCPConfig(config: MCPConfigFile, source: SourceMeta): MCPServe
41
48
 
42
49
  if (config.mcpServers) {
43
50
  for (const [name, serverConfig] of Object.entries(config.mcpServers)) {
51
+ // Runtime type validation for user-controlled JSON values
52
+ let enabled: boolean | undefined;
53
+ if (serverConfig.enabled !== undefined) {
54
+ if (typeof serverConfig.enabled === "boolean") {
55
+ enabled = serverConfig.enabled;
56
+ } else {
57
+ logger.warn("MCP server has invalid 'enabled' value, ignoring", { name, value: serverConfig.enabled });
58
+ }
59
+ }
60
+
61
+ let timeout: number | undefined;
62
+ if (serverConfig.timeout !== undefined) {
63
+ if (
64
+ typeof serverConfig.timeout === "number" &&
65
+ Number.isFinite(serverConfig.timeout) &&
66
+ serverConfig.timeout > 0
67
+ ) {
68
+ timeout = serverConfig.timeout;
69
+ } else {
70
+ logger.warn("MCP server has invalid 'timeout' value, ignoring", { name, value: serverConfig.timeout });
71
+ }
72
+ }
73
+
44
74
  const server: MCPServer = {
45
75
  name,
76
+ enabled,
77
+ timeout,
46
78
  command: serverConfig.command,
47
79
  args: serverConfig.args,
48
80
  env: serverConfig.env,
49
81
  url: serverConfig.url,
50
82
  headers: serverConfig.headers,
83
+ auth: serverConfig.auth,
51
84
  transport: serverConfig.type,
52
85
  _source: source,
53
86
  };
@@ -34,6 +34,7 @@ export const BUILTIN_SLASH_COMMANDS: ReadonlyArray<BuiltinSlashCommand> = [
34
34
  { name: "tree", description: "Navigate session tree (switch branches)" },
35
35
  { name: "login", description: "Login with OAuth provider" },
36
36
  { name: "logout", description: "Logout from OAuth provider" },
37
+ { name: "mcp", description: "Manage MCP servers (add, list, remove, test)" },
37
38
  { name: "new", description: "Start a new session" },
38
39
  { name: "compact", description: "Manually compact the session context" },
39
40
  { name: "handoff", description: "Hand off session context to a new session" },
package/src/index.ts CHANGED
@@ -248,10 +248,8 @@ export {
248
248
  type FindToolInput,
249
249
  type FindToolOptions,
250
250
  formatSize,
251
- type GrepOperations,
252
251
  type GrepToolDetails,
253
252
  type GrepToolInput,
254
- type GrepToolOptions,
255
253
  type PythonToolDetails,
256
254
  type ReadToolDetails,
257
255
  type ReadToolInput,
@@ -0,0 +1,194 @@
1
+ /**
2
+ * MCP Configuration File Writer
3
+ *
4
+ * Utilities for reading/writing .omp/mcp.json files at user or project level.
5
+ */
6
+ import * as fs from "node:fs";
7
+ import * as os from "node:os";
8
+ import * as path from "node:path";
9
+ import { isEnoent } from "@oh-my-pi/pi-utils";
10
+ import { validateServerConfig } from "./config";
11
+ import type { MCPConfigFile, MCPServerConfig } from "./types";
12
+
13
+ /**
14
+ * Get the path to the MCP config file.
15
+ * @param scope - "user" for ~/.omp/mcp.json or "project" for .omp/mcp.json
16
+ * @param cwd - Current working directory (used for project scope)
17
+ */
18
+ export function getMCPConfigPath(scope: "user" | "project", cwd: string): string {
19
+ if (scope === "user") {
20
+ return path.join(os.homedir(), ".omp", "mcp.json");
21
+ }
22
+ return path.join(cwd, ".omp", "mcp.json");
23
+ }
24
+
25
+ /**
26
+ * Read an MCP config file.
27
+ * Returns empty config if file doesn't exist.
28
+ */
29
+ export async function readMCPConfigFile(filePath: string): Promise<MCPConfigFile> {
30
+ try {
31
+ const content = await fs.promises.readFile(filePath, "utf-8");
32
+ const parsed = JSON.parse(content) as MCPConfigFile;
33
+ return parsed;
34
+ } catch (error) {
35
+ if (isEnoent(error)) {
36
+ // File doesn't exist, return empty config
37
+ return { mcpServers: {} };
38
+ }
39
+ throw error;
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Write an MCP config file atomically.
45
+ * Creates parent directories if they don't exist.
46
+ */
47
+ export async function writeMCPConfigFile(filePath: string, config: MCPConfigFile): Promise<void> {
48
+ // Ensure parent directory exists
49
+ const dir = path.dirname(filePath);
50
+ await fs.promises.mkdir(dir, { recursive: true, mode: 0o700 });
51
+
52
+ // Write to temp file first (atomic write)
53
+ const tmpPath = `${filePath}.tmp`;
54
+ const content = JSON.stringify(config, null, 2);
55
+ await fs.promises.writeFile(tmpPath, content, { encoding: "utf-8", mode: 0o600 });
56
+
57
+ // Rename to final path (atomic on most systems)
58
+ await fs.promises.rename(tmpPath, filePath);
59
+ }
60
+
61
+ /**
62
+ * Validate server name.
63
+ * @returns Error message if invalid, undefined if valid
64
+ */
65
+ export function validateServerName(name: string): string | undefined {
66
+ if (!name) {
67
+ return "Server name cannot be empty";
68
+ }
69
+ if (name.length > 100) {
70
+ return "Server name is too long (max 100 characters)";
71
+ }
72
+ // Check for invalid characters (only allow alphanumeric, dash, underscore, dot)
73
+ if (!/^[a-zA-Z0-9_.-]+$/.test(name)) {
74
+ return "Server name can only contain letters, numbers, dash, underscore, and dot";
75
+ }
76
+ return undefined;
77
+ }
78
+
79
+ /**
80
+ * Add an MCP server to a config file.
81
+ * Validates the config before writing.
82
+ *
83
+ * @throws Error if server name already exists or validation fails
84
+ */
85
+ export async function addMCPServer(filePath: string, name: string, config: MCPServerConfig): Promise<void> {
86
+ // Validate server name
87
+ const nameError = validateServerName(name);
88
+ if (nameError) {
89
+ throw new Error(nameError);
90
+ }
91
+
92
+ // Validate the config
93
+ const errors = validateServerConfig(name, config);
94
+ if (errors.length > 0) {
95
+ throw new Error(`Invalid server config: ${errors.join("; ")}`);
96
+ }
97
+
98
+ // Read existing config
99
+ const existing = await readMCPConfigFile(filePath);
100
+
101
+ // Check for duplicate name
102
+ if (existing.mcpServers?.[name]) {
103
+ throw new Error(`Server "${name}" already exists in ${filePath}`);
104
+ }
105
+
106
+ // Add server
107
+ const updated: MCPConfigFile = {
108
+ ...existing,
109
+ mcpServers: {
110
+ ...existing.mcpServers,
111
+ [name]: config,
112
+ },
113
+ };
114
+
115
+ // Write back
116
+ await writeMCPConfigFile(filePath, updated);
117
+ }
118
+
119
+ /**
120
+ * Update an existing MCP server in a config file.
121
+ * If the server doesn't exist, this will add it.
122
+ *
123
+ * @throws Error if validation fails
124
+ */
125
+ export async function updateMCPServer(filePath: string, name: string, config: MCPServerConfig): Promise<void> {
126
+ // Validate server name
127
+ const nameError = validateServerName(name);
128
+ if (nameError) {
129
+ throw new Error(nameError);
130
+ }
131
+
132
+ // Validate the config
133
+ const errors = validateServerConfig(name, config);
134
+ if (errors.length > 0) {
135
+ throw new Error(`Invalid server config: ${errors.join("; ")}`);
136
+ }
137
+
138
+ // Read existing config
139
+ const existing = await readMCPConfigFile(filePath);
140
+
141
+ // Update server
142
+ const updated: MCPConfigFile = {
143
+ ...existing,
144
+ mcpServers: {
145
+ ...existing.mcpServers,
146
+ [name]: config,
147
+ },
148
+ };
149
+
150
+ // Write back
151
+ await writeMCPConfigFile(filePath, updated);
152
+ }
153
+
154
+ /**
155
+ * Remove an MCP server from a config file.
156
+ *
157
+ * @throws Error if server doesn't exist
158
+ */
159
+ export async function removeMCPServer(filePath: string, name: string): Promise<void> {
160
+ // Read existing config
161
+ const existing = await readMCPConfigFile(filePath);
162
+
163
+ // Check if server exists
164
+ if (!existing.mcpServers?.[name]) {
165
+ throw new Error(`Server "${name}" not found in ${filePath}`);
166
+ }
167
+
168
+ // Remove server
169
+ const { [name]: _removed, ...remaining } = existing.mcpServers;
170
+ const updated: MCPConfigFile = {
171
+ ...existing,
172
+ mcpServers: remaining,
173
+ };
174
+
175
+ // Write back
176
+ await writeMCPConfigFile(filePath, updated);
177
+ }
178
+
179
+ /**
180
+ * Get a specific server config from a file.
181
+ * Returns undefined if server doesn't exist.
182
+ */
183
+ export async function getMCPServer(filePath: string, name: string): Promise<MCPServerConfig | undefined> {
184
+ const config = await readMCPConfigFile(filePath);
185
+ return config.mcpServers?.[name];
186
+ }
187
+
188
+ /**
189
+ * List all server names in a config file.
190
+ */
191
+ export async function listMCPServers(filePath: string): Promise<string[]> {
192
+ const config = await readMCPConfigFile(filePath);
193
+ return Object.keys(config.mcpServers ?? {});
194
+ }
package/src/mcp/config.ts CHANGED
@@ -4,6 +4,7 @@
4
4
  * Uses the capability system to load MCP servers from multiple sources.
5
5
  */
6
6
  import { mcpCapability } from "../capability/mcp";
7
+ import type { SourceMeta } from "../capability/types";
7
8
  import type { MCPServer } from "../discovery";
8
9
  import { loadCapability } from "../discovery";
9
10
  import type { MCPServerConfig } from "./types";
@@ -23,7 +24,7 @@ export interface LoadMCPConfigsResult {
23
24
  /** Extracted Exa API keys (if any were filtered) */
24
25
  exaApiKeys: string[];
25
26
  /** Source metadata for each server */
26
- sources: Record<string, import("../capability/types").SourceMeta>;
27
+ sources: Record<string, SourceMeta>;
27
28
  }
28
29
 
29
30
  /**
@@ -32,9 +33,15 @@ export interface LoadMCPConfigsResult {
32
33
  function convertToLegacyConfig(server: MCPServer): MCPServerConfig {
33
34
  // Determine transport type
34
35
  const transport = server.transport ?? (server.command ? "stdio" : server.url ? "http" : "stdio");
36
+ const shared = {
37
+ enabled: server.enabled,
38
+ timeout: server.timeout,
39
+ auth: server.auth,
40
+ };
35
41
 
36
42
  if (transport === "stdio") {
37
43
  const config: MCPServerConfig = {
44
+ ...shared,
38
45
  type: "stdio" as const,
39
46
  command: server.command ?? "",
40
47
  };
@@ -45,6 +52,7 @@ function convertToLegacyConfig(server: MCPServer): MCPServerConfig {
45
52
 
46
53
  if (transport === "http") {
47
54
  const config: MCPServerConfig = {
55
+ ...shared,
48
56
  type: "http" as const,
49
57
  url: server.url ?? "",
50
58
  };
@@ -54,6 +62,7 @@ function convertToLegacyConfig(server: MCPServer): MCPServerConfig {
54
62
 
55
63
  if (transport === "sse") {
56
64
  const config: MCPServerConfig = {
65
+ ...shared,
57
66
  type: "sse" as const,
58
67
  url: server.url ?? "",
59
68
  };
@@ -63,6 +72,7 @@ function convertToLegacyConfig(server: MCPServer): MCPServerConfig {
63
72
 
64
73
  // Fallback to stdio
65
74
  return {
75
+ ...shared,
66
76
  type: "stdio" as const,
67
77
  command: server.command ?? "",
68
78
  };
@@ -89,9 +99,13 @@ export async function loadAllMCPConfigs(cwd: string, options?: LoadMCPConfigsOpt
89
99
 
90
100
  // Convert to legacy format and preserve source metadata
91
101
  const configs: Record<string, MCPServerConfig> = {};
92
- const sources: Record<string, import("../capability/types").SourceMeta> = {};
102
+ const sources: Record<string, SourceMeta> = {};
93
103
  for (const server of servers) {
94
- configs[server.name] = convertToLegacyConfig(server);
104
+ const config = convertToLegacyConfig(server);
105
+ if (config.enabled === false) {
106
+ continue;
107
+ }
108
+ configs[server.name] = config;
95
109
  sources[server.name] = server._source;
96
110
  }
97
111
 
@@ -179,7 +193,7 @@ export interface ExaFilterResult {
179
193
  /** Extracted Exa API keys (if any) */
180
194
  exaApiKeys: string[];
181
195
  /** Source metadata for remaining servers */
182
- sources: Record<string, import("../capability/types").SourceMeta>;
196
+ sources: Record<string, SourceMeta>;
183
197
  }
184
198
 
185
199
  /**
@@ -188,10 +202,10 @@ export interface ExaFilterResult {
188
202
  */
189
203
  export function filterExaMCPServers(
190
204
  configs: Record<string, MCPServerConfig>,
191
- sources: Record<string, import("../capability/types").SourceMeta>,
205
+ sources: Record<string, SourceMeta>,
192
206
  ): ExaFilterResult {
193
207
  const filtered: Record<string, MCPServerConfig> = {};
194
- const filteredSources: Record<string, import("../capability/types").SourceMeta> = {};
208
+ const filteredSources: Record<string, SourceMeta> = {};
195
209
  const exaApiKeys: string[] = [];
196
210
 
197
211
  for (const [name, config] of Object.entries(configs)) {
package/src/mcp/index.ts CHANGED
@@ -16,6 +16,7 @@ export {
16
16
  loadAllMCPConfigs,
17
17
  validateServerConfig,
18
18
  } from "./config";
19
+ export { validateServerName } from "./config-writer";
19
20
  // JSON-RPC (lightweight HTTP-based MCP calls)
20
21
  export type { JsonRpcResponse } from "./json-rpc";
21
22
  export { callMCP, parseSSE } from "./json-rpc";
@@ -25,6 +26,9 @@ export { discoverAndLoadMCPTools } from "./loader";
25
26
  // Manager
26
27
  export type { MCPDiscoverOptions, MCPLoadResult } from "./manager";
27
28
  export { createMCPManager, MCPManager } from "./manager";
29
+ // OAuth Discovery
30
+ export type { AuthDetectionResult, OAuthEndpoints } from "./oauth-discovery";
31
+ export { analyzeAuthError, detectAuthError, discoverOAuthEndpoints, extractOAuthEndpoints } from "./oauth-discovery";
28
32
  // Tool bridge
29
33
  export type { MCPToolDetails } from "./tool-bridge";
30
34
  export { createMCPToolName, DeferredMCPTool, MCPTool, parseMCPToolName } from "./tool-bridge";
package/src/mcp/loader.ts CHANGED
@@ -6,6 +6,7 @@
6
6
  import { logger } from "@oh-my-pi/pi-utils";
7
7
  import type { LoadedCustomTool } from "../extensibility/custom-tools/types";
8
8
  import { AgentStorage } from "../session/agent-storage";
9
+ import type { AuthStorage } from "../session/auth-storage";
9
10
  import { type MCPLoadResult, MCPManager } from "./manager";
10
11
  import { MCPToolCache } from "./tool-cache";
11
12
 
@@ -33,6 +34,8 @@ export interface MCPToolsLoadOptions {
33
34
  filterExa?: boolean;
34
35
  /** SQLite storage for MCP tool cache (null disables cache) */
35
36
  cacheStorage?: AgentStorage | null;
37
+ /** Auth storage used to resolve OAuth credentials before initial MCP connect */
38
+ authStorage?: AuthStorage;
36
39
  }
37
40
 
38
41
  async function resolveToolCache(storage: AgentStorage | null | undefined): Promise<MCPToolCache | null> {
@@ -56,6 +59,9 @@ async function resolveToolCache(storage: AgentStorage | null | undefined): Promi
56
59
  export async function discoverAndLoadMCPTools(cwd: string, options?: MCPToolsLoadOptions): Promise<MCPToolsLoadResult> {
57
60
  const toolCache = await resolveToolCache(options?.cacheStorage);
58
61
  const manager = new MCPManager(cwd, toolCache);
62
+ if (options?.authStorage) {
63
+ manager.setAuthStorage(options.authStorage);
64
+ }
59
65
 
60
66
  let result: MCPLoadResult;
61
67
  try {