@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 +42 -0
- package/package.json +7 -7
- package/src/capability/mcp.ts +9 -0
- package/src/config/file-lock.ts +1 -1
- package/src/discovery/builtin.ts +48 -0
- package/src/discovery/mcp-json.ts +33 -0
- package/src/extensibility/slash-commands.ts +1 -0
- package/src/index.ts +0 -2
- package/src/mcp/config-writer.ts +194 -0
- package/src/mcp/config.ts +20 -6
- package/src/mcp/index.ts +4 -0
- package/src/mcp/loader.ts +6 -0
- package/src/mcp/manager.ts +92 -3
- package/src/mcp/oauth-discovery.ts +274 -0
- package/src/mcp/oauth-flow.ts +229 -0
- package/src/mcp/tool-bridge.ts +8 -8
- package/src/mcp/transports/http.ts +76 -35
- package/src/mcp/transports/stdio.ts +31 -16
- package/src/mcp/types.ts +15 -1
- package/src/modes/components/mcp-add-wizard.ts +1286 -0
- package/src/modes/components/tool-execution.ts +12 -24
- package/src/modes/controllers/input-controller.ts +8 -0
- package/src/modes/controllers/mcp-command-controller.ts +1223 -0
- package/src/modes/interactive-mode.ts +6 -0
- package/src/modes/types.ts +1 -0
- package/src/sdk.ts +1 -0
- package/src/session/agent-session.ts +49 -0
- package/src/system-prompt.ts +2 -3
- package/src/task/executor.ts +26 -38
- package/src/task/worktree.ts +8 -5
- package/src/tools/bash.ts +8 -4
- package/src/tools/browser.ts +7 -4
- package/src/tools/grep.ts +1 -13
- package/src/tools/index.ts +1 -1
- package/src/utils/event-bus.ts +3 -1
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.
|
|
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.
|
|
94
|
-
"@oh-my-pi/pi-agent-core": "11.
|
|
95
|
-
"@oh-my-pi/pi-ai": "11.
|
|
96
|
-
"@oh-my-pi/pi-natives": "11.
|
|
97
|
-
"@oh-my-pi/pi-tui": "11.
|
|
98
|
-
"@oh-my-pi/pi-utils": "11.
|
|
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",
|
package/src/capability/mcp.ts
CHANGED
|
@@ -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) */
|
package/src/config/file-lock.ts
CHANGED
|
@@ -101,7 +101,7 @@ async function acquireLock(filePath: string, options: FileLockOptions = {}): Pro
|
|
|
101
101
|
continue;
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
-
await
|
|
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`);
|
package/src/discovery/builtin.ts
CHANGED
|
@@ -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,
|
|
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,
|
|
102
|
+
const sources: Record<string, SourceMeta> = {};
|
|
93
103
|
for (const server of servers) {
|
|
94
|
-
|
|
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,
|
|
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,
|
|
205
|
+
sources: Record<string, SourceMeta>,
|
|
192
206
|
): ExaFilterResult {
|
|
193
207
|
const filtered: Record<string, MCPServerConfig> = {};
|
|
194
|
-
const filteredSources: Record<string,
|
|
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 {
|