@meowlynxsea/koi 0.1.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.
Files changed (109) hide show
  1. package/LICENSE +34 -0
  2. package/NOTICE +35 -0
  3. package/README.md +15 -0
  4. package/bin/koi +12 -0
  5. package/dist/highlights-eq9cgrbb.scm +604 -0
  6. package/dist/highlights-ghv9g403.scm +205 -0
  7. package/dist/highlights-hk7bwhj4.scm +284 -0
  8. package/dist/highlights-r812a2qc.scm +150 -0
  9. package/dist/highlights-x6tmsnaa.scm +115 -0
  10. package/dist/injections-73j83es3.scm +27 -0
  11. package/dist/main.js +489918 -0
  12. package/dist/tree-sitter-javascript-nd0q4pe9.wasm +0 -0
  13. package/dist/tree-sitter-markdown-411r6y9b.wasm +0 -0
  14. package/dist/tree-sitter-markdown_inline-j5349f42.wasm +0 -0
  15. package/dist/tree-sitter-typescript-zxjzwt75.wasm +0 -0
  16. package/dist/tree-sitter-zig-e78zbjpm.wasm +0 -0
  17. package/package.json +51 -0
  18. package/src/agent/check-permissions.ts +239 -0
  19. package/src/agent/hooks/message-utils.ts +305 -0
  20. package/src/agent/hooks/types.ts +32 -0
  21. package/src/agent/hooks.ts +1560 -0
  22. package/src/agent/mode.ts +163 -0
  23. package/src/agent/monitor-registry.ts +308 -0
  24. package/src/agent/permission-ui.ts +71 -0
  25. package/src/agent/plan-ui.ts +74 -0
  26. package/src/agent/question-ui.ts +58 -0
  27. package/src/agent/session-fork.ts +299 -0
  28. package/src/agent/session-snapshots.ts +216 -0
  29. package/src/agent/session-store.ts +649 -0
  30. package/src/agent/session-tasks.ts +305 -0
  31. package/src/agent/session.ts +27 -0
  32. package/src/agent/subagent-registry.ts +176 -0
  33. package/src/agent/subagent.ts +194 -0
  34. package/src/agent/tool-orchestration.ts +55 -0
  35. package/src/agent/tools.ts +8 -0
  36. package/src/cli/args.ts +6 -0
  37. package/src/cli/commands.ts +5 -0
  38. package/src/commands/skills/index.ts +23 -0
  39. package/src/config/models.ts +6 -0
  40. package/src/config/settings.ts +392 -0
  41. package/src/main.tsx +64 -0
  42. package/src/services/mcp/client.ts +194 -0
  43. package/src/services/mcp/config.ts +232 -0
  44. package/src/services/mcp/connection-manager.ts +258 -0
  45. package/src/services/mcp/index.ts +80 -0
  46. package/src/services/mcp/mcp-commands.ts +114 -0
  47. package/src/services/mcp/stdio-transport.ts +246 -0
  48. package/src/services/mcp/types.ts +155 -0
  49. package/src/skills/SkillsMenu.tsx +370 -0
  50. package/src/skills/bundled/batch.ts +106 -0
  51. package/src/skills/bundled/debug.ts +86 -0
  52. package/src/skills/bundled/loremIpsum.ts +101 -0
  53. package/src/skills/bundled/remember.ts +97 -0
  54. package/src/skills/bundled/simplify.ts +100 -0
  55. package/src/skills/bundled/skillify.ts +123 -0
  56. package/src/skills/bundled/stuck.ts +101 -0
  57. package/src/skills/bundled/updateConfig.ts +228 -0
  58. package/src/skills/bundled.ts +46 -0
  59. package/src/skills/frontmatter.ts +179 -0
  60. package/src/skills/index.ts +87 -0
  61. package/src/skills/invoke.ts +231 -0
  62. package/src/skills/loader.ts +710 -0
  63. package/src/skills/substitution.ts +169 -0
  64. package/src/skills/types.ts +201 -0
  65. package/src/tools/agent.ts +143 -0
  66. package/src/tools/ask-user-question.ts +46 -0
  67. package/src/tools/bash.ts +148 -0
  68. package/src/tools/edit.ts +164 -0
  69. package/src/tools/glob.ts +102 -0
  70. package/src/tools/grep.ts +248 -0
  71. package/src/tools/index.ts +73 -0
  72. package/src/tools/list-mcp-resources.ts +74 -0
  73. package/src/tools/ls.ts +85 -0
  74. package/src/tools/mcp.ts +76 -0
  75. package/src/tools/monitor.ts +159 -0
  76. package/src/tools/plan-mode.ts +134 -0
  77. package/src/tools/read-mcp-resource.ts +79 -0
  78. package/src/tools/read.ts +137 -0
  79. package/src/tools/skill.ts +176 -0
  80. package/src/tools/task.ts +349 -0
  81. package/src/tools/types.ts +52 -0
  82. package/src/tools/webfetch-domains.ts +239 -0
  83. package/src/tools/webfetch.ts +533 -0
  84. package/src/tools/write.ts +101 -0
  85. package/src/tui/app.tsx +1178 -0
  86. package/src/tui/components/chat-panel.tsx +1071 -0
  87. package/src/tui/components/command-panel.tsx +261 -0
  88. package/src/tui/components/confirm-modal.tsx +135 -0
  89. package/src/tui/components/connect-modal.tsx +435 -0
  90. package/src/tui/components/connecting-modal.tsx +167 -0
  91. package/src/tui/components/edit-pending-modal.tsx +103 -0
  92. package/src/tui/components/exit-modal.tsx +131 -0
  93. package/src/tui/components/fork-modal.tsx +377 -0
  94. package/src/tui/components/image-preview-modal.tsx +141 -0
  95. package/src/tui/components/image-utils.ts +128 -0
  96. package/src/tui/components/info-bar.tsx +103 -0
  97. package/src/tui/components/input-box.tsx +352 -0
  98. package/src/tui/components/mcp/MCPSettings.tsx +386 -0
  99. package/src/tui/components/mcp/index.ts +7 -0
  100. package/src/tui/components/model-modal.tsx +310 -0
  101. package/src/tui/components/pending-area.tsx +88 -0
  102. package/src/tui/components/rename-modal.tsx +119 -0
  103. package/src/tui/components/session-modal.tsx +233 -0
  104. package/src/tui/components/side-bar.tsx +349 -0
  105. package/src/tui/components/tool-output.ts +6 -0
  106. package/src/tui/hooks/user-prompt-history.ts +114 -0
  107. package/src/tui/theme.ts +63 -0
  108. package/src/types/commands.ts +80 -0
  109. package/src/types/cross-spawn.d.ts +24 -0
@@ -0,0 +1,194 @@
1
+ /**
2
+ * MCP Client Connection
3
+ */
4
+
5
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
6
+ import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
7
+ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
8
+ import type { Tool, Resource, ServerCapabilities } from "@modelcontextprotocol/sdk/types.js";
9
+ import type { ScopedMcpConfig, ConnectedMCPServer } from "./types.js";
10
+ import { FilteredStdioClientTransport } from "./stdio-transport.js";
11
+
12
+ const CONNECTION_TIMEOUT = 60000;
13
+
14
+ async function createTransport(config: ScopedMcpConfig) {
15
+ if (config.type === "stdio" && config.command) {
16
+ return new FilteredStdioClientTransport({
17
+ command: config.command,
18
+ args: config.args ?? [],
19
+ env: config.env,
20
+ });
21
+ }
22
+
23
+ if (config.url) {
24
+ const type = config.type ?? "http";
25
+ const headers: Record<string, string> = { ...(config.headers ?? {}) };
26
+ if (config.authToken) {
27
+ headers["Authorization"] = `Bearer ${config.authToken}`;
28
+ }
29
+ const url = new URL(config.url);
30
+
31
+ switch (type) {
32
+ case "sse":
33
+ return new SSEClientTransport(url);
34
+ default:
35
+ return new StreamableHTTPClientTransport(url);
36
+ }
37
+ }
38
+
39
+ throw new Error("Invalid MCP server configuration");
40
+ }
41
+
42
+ export interface ConnectResult {
43
+ success: boolean;
44
+ server?: ConnectedMCPServer;
45
+ error?: string;
46
+ }
47
+
48
+ export async function connectToServer(
49
+ name: string,
50
+ config: ScopedMcpConfig,
51
+ options?: { timeout?: number; onProgress?: (message: string) => void }
52
+ ): Promise<ConnectResult> {
53
+ const timeout = options?.timeout ?? CONNECTION_TIMEOUT;
54
+
55
+ try {
56
+ options?.onProgress?.(`Connecting to ${name}...`);
57
+ const transport = await createTransport(config);
58
+
59
+ const client = new Client(
60
+ { name: "koi", version: "1.0.0" },
61
+ { capabilities: {} }
62
+ );
63
+
64
+ const abortController = new AbortController();
65
+ const timeoutId = setTimeout(() => abortController.abort(), timeout);
66
+
67
+ try {
68
+ await client.connect(transport);
69
+ clearTimeout(timeoutId);
70
+ options?.onProgress?.(`Connected to ${name}`);
71
+
72
+ // Make these optional - not all MCP servers support all methods
73
+ let serverInfo: Record<string, unknown> = { name: "unknown", version: "0.0.0" };
74
+ try {
75
+ const versionResult = client.getServerVersion();
76
+ // Handle both sync and async versions
77
+ if (versionResult instanceof Promise) {
78
+ serverInfo = await versionResult as Record<string, unknown>;
79
+ } else {
80
+ serverInfo = versionResult as Record<string, unknown>;
81
+ }
82
+ } catch {
83
+ // Server may not support getServerVersion
84
+ }
85
+
86
+ let toolsResponse = { tools: [] as Tool[] };
87
+ try {
88
+ const result = client.listTools();
89
+ toolsResponse = "then" in result ? await result : result;
90
+ } catch {
91
+ // Server may not support tools
92
+ }
93
+
94
+ let resourcesResponse = { resources: [] as Resource[] };
95
+ try {
96
+ const result = client.listResources();
97
+ resourcesResponse = "then" in result ? await result : result;
98
+ } catch {
99
+ // Server may not support resources
100
+ }
101
+
102
+ let instructions: string | undefined;
103
+ try {
104
+ instructions = client.getInstructions();
105
+ } catch {
106
+ // Instructions not available
107
+ }
108
+
109
+ const cleanup = async () => {
110
+ try {
111
+ await client.close();
112
+ } catch {
113
+ // Ignore cleanup errors
114
+ }
115
+ };
116
+
117
+ // Access properties using bracket notation to avoid TS errors
118
+ const info = serverInfo as Record<string, unknown>;
119
+ const capabilities = (info["capabilities"] as ServerCapabilities | undefined) ?? {};
120
+ const infoName = (info["name"] as string | undefined) ?? "unknown";
121
+ const infoVersion = (info["version"] as string | undefined) ?? "0.0.0";
122
+
123
+ const server: ConnectedMCPServer = {
124
+ client,
125
+ name,
126
+ status: "connected",
127
+ capabilities,
128
+ serverInfo: { name: infoName, version: infoVersion },
129
+ instructions,
130
+ config,
131
+ tools: (toolsResponse.tools ?? []) as Tool[],
132
+ resources: (resourcesResponse.resources ?? []) as Resource[],
133
+ cleanup,
134
+ };
135
+
136
+ return { success: true, server };
137
+ } catch (connectError) {
138
+ clearTimeout(timeoutId);
139
+ throw connectError;
140
+ }
141
+ } catch (error) {
142
+ const errorMessage = error instanceof Error ? error.message : String(error);
143
+ options?.onProgress?.(`Failed to connect to ${name}: ${errorMessage}`);
144
+ return { success: false, error: errorMessage };
145
+ }
146
+ }
147
+
148
+ export async function disconnectFromServer(server: ConnectedMCPServer): Promise<void> {
149
+ await server.cleanup();
150
+ }
151
+
152
+ export async function callMcpTool(
153
+ server: ConnectedMCPServer,
154
+ toolName: string,
155
+ args: Record<string, unknown>
156
+ ): Promise<{ success: boolean; content?: Array<Record<string, unknown>>; error?: string }> {
157
+ try {
158
+ const result = await server.client.callTool({ name: toolName, arguments: args });
159
+ return { success: true, content: result.content as Array<Record<string, unknown>> };
160
+ } catch (error) {
161
+ const errorMessage = error instanceof Error ? error.message : String(error);
162
+ return { success: false, error: errorMessage };
163
+ }
164
+ }
165
+
166
+ export async function listMcpResources(server: ConnectedMCPServer) {
167
+ const response = await server.client.listResources();
168
+ return response.resources ?? [];
169
+ }
170
+
171
+ export async function readMcpResource(server: ConnectedMCPServer, uri: string) {
172
+ try {
173
+ return await server.client.readResource({ uri });
174
+ } catch {
175
+ return null;
176
+ }
177
+ }
178
+
179
+ export async function listMcpPrompts(server: ConnectedMCPServer) {
180
+ const response = await server.client.listPrompts();
181
+ return response.prompts ?? [];
182
+ }
183
+
184
+ export async function executeMcpPrompt(
185
+ server: ConnectedMCPServer,
186
+ promptName: string,
187
+ args?: Record<string, string>
188
+ ) {
189
+ try {
190
+ return await server.client.getPrompt({ name: promptName, arguments: args });
191
+ } catch {
192
+ return null;
193
+ }
194
+ }
@@ -0,0 +1,232 @@
1
+ /**
2
+ * MCP Configuration Management
3
+ */
4
+
5
+ import fs from "fs";
6
+ import path from "path";
7
+ import os from "os";
8
+ import type { McpJsonConfig, ScopedMcpConfig, McpServerConfig, ConnectionStatus } from "./types.js";
9
+
10
+ const KOI_CONFIG_DIR = path.join(os.homedir(), ".config", "koi");
11
+ const MCP_CONFIG_FILE = path.join(KOI_CONFIG_DIR, "mcp.json");
12
+
13
+ const mcpConfigs: Map<string, ScopedMcpConfig> = new Map();
14
+ const disabledServers: Set<string> = new Set();
15
+ let configsLoaded = false;
16
+
17
+ function ensureConfigDir(): void {
18
+ if (!fs.existsSync(KOI_CONFIG_DIR)) {
19
+ fs.mkdirSync(KOI_CONFIG_DIR, { recursive: true, mode: 0o700 });
20
+ }
21
+ }
22
+
23
+ function loadConfigFile(): void {
24
+ if (!fs.existsSync(MCP_CONFIG_FILE)) return;
25
+ try {
26
+ const raw = fs.readFileSync(MCP_CONFIG_FILE, "utf-8");
27
+ const data = JSON.parse(raw) as McpJsonConfig;
28
+ if (data.mcpServers) {
29
+ for (const [name, config] of Object.entries(data.mcpServers)) {
30
+ const cfg = config as McpServerConfig;
31
+ if (cfg.command || cfg.url) {
32
+ const scopedConfig: ScopedMcpConfig = { ...cfg, scope: "user" };
33
+ mcpConfigs.set(name, scopedConfig);
34
+ // Load disabled state from config
35
+ if (cfg.enabled === false) {
36
+ disabledServers.add(name);
37
+ }
38
+ }
39
+ }
40
+ }
41
+ } catch {
42
+ // Ignore corrupt config files
43
+ }
44
+ }
45
+
46
+ function saveConfigFile(): void {
47
+ ensureConfigDir();
48
+ const servers: Record<string, McpServerConfig> = {};
49
+ for (const [name, scopedConfig] of mcpConfigs) {
50
+ if (scopedConfig.scope === "user") {
51
+ const { scope: _scope, description: _desc, ...config } = scopedConfig;
52
+ // Preserve the enabled field for disabled servers
53
+ if (disabledServers.has(name)) {
54
+ servers[name] = { ...config, enabled: false };
55
+ } else {
56
+ servers[name] = config;
57
+ }
58
+ }
59
+ }
60
+ fs.writeFileSync(MCP_CONFIG_FILE, JSON.stringify({ mcpServers: servers }, null, 2) + "\n", { mode: 0o600 });
61
+ }
62
+
63
+ export function loadMcpConfigs(): void {
64
+ if (configsLoaded) return;
65
+ loadConfigFile();
66
+ configsLoaded = true;
67
+ }
68
+
69
+ export function loadProjectMcpConfig(cwd: string): ScopedMcpConfig[] {
70
+ const configPath = path.join(cwd, ".mcp.json");
71
+ if (!fs.existsSync(configPath)) return [];
72
+ try {
73
+ const raw = fs.readFileSync(configPath, "utf-8");
74
+ const data = JSON.parse(raw) as McpJsonConfig;
75
+ const configs: ScopedMcpConfig[] = [];
76
+ if (data.mcpServers) {
77
+ for (const [, config] of Object.entries(data.mcpServers)) {
78
+ const cfg = config as McpServerConfig;
79
+ if (cfg.command || cfg.url) {
80
+ configs.push({ ...cfg, scope: "project" });
81
+ }
82
+ }
83
+ }
84
+ return configs;
85
+ } catch {
86
+ return [];
87
+ }
88
+ }
89
+
90
+ export function loadLocalMcpConfig(cwd: string): ScopedMcpConfig[] {
91
+ const configPath = path.join(cwd, ".mcp.json.local");
92
+ if (!fs.existsSync(configPath)) return [];
93
+ try {
94
+ const raw = fs.readFileSync(configPath, "utf-8");
95
+ const data = JSON.parse(raw) as McpJsonConfig;
96
+ const configs: ScopedMcpConfig[] = [];
97
+ if (data.mcpServers) {
98
+ for (const [, config] of Object.entries(data.mcpServers)) {
99
+ const cfg = config as McpServerConfig;
100
+ if (cfg.command || cfg.url) {
101
+ configs.push({ ...cfg, scope: "local" });
102
+ }
103
+ }
104
+ }
105
+ return configs;
106
+ } catch {
107
+ return [];
108
+ }
109
+ }
110
+
111
+ export function setMcpConfig(name: string, config: McpServerConfig, scope: "user" | "project" | "local" = "user"): void {
112
+ mcpConfigs.set(name, { ...config, scope });
113
+ if (scope === "user") saveConfigFile();
114
+ }
115
+
116
+ export function getMcpConfig(name: string): ScopedMcpConfig | undefined {
117
+ return mcpConfigs.get(name);
118
+ }
119
+
120
+ export function getAllMcpConfigs(): Map<string, ScopedMcpConfig> {
121
+ return new Map(mcpConfigs);
122
+ }
123
+
124
+ export function getMcpConfigsByScope(scope: "user" | "project" | "local"): ScopedMcpConfig[] {
125
+ return Array.from(mcpConfigs.values()).filter(c => c.scope === scope);
126
+ }
127
+
128
+ export function removeMcpConfig(name: string): boolean {
129
+ const config = mcpConfigs.get(name);
130
+ if (!config) return false;
131
+ mcpConfigs.delete(name);
132
+ disabledServers.delete(name);
133
+ if (config.scope === "user") saveConfigFile();
134
+ return true;
135
+ }
136
+
137
+ export function isMcpServerDisabled(name: string): boolean {
138
+ return disabledServers.has(name);
139
+ }
140
+
141
+ export function setMcpServerEnabled(name: string, enabled: boolean): void {
142
+ if (enabled) {
143
+ disabledServers.delete(name);
144
+ } else {
145
+ disabledServers.add(name);
146
+ }
147
+ // Persist the disabled state to disk
148
+ const config = mcpConfigs.get(name);
149
+ if (config && config.scope === "user") {
150
+ saveConfigFile();
151
+ }
152
+ }
153
+
154
+ export function getMcpServerNames(): string[] {
155
+ return Array.from(mcpConfigs.keys());
156
+ }
157
+
158
+ const serverStatuses: Map<string, ConnectionStatus> = new Map();
159
+
160
+ export function updateServerStatus(name: string, status: ConnectionStatus): void {
161
+ serverStatuses.set(name, status);
162
+ }
163
+
164
+ export function getServerStatus(name: string): ConnectionStatus | undefined {
165
+ return serverStatuses.get(name);
166
+ }
167
+
168
+ export function getAllServerStatuses(): Map<string, ConnectionStatus> {
169
+ return new Map(serverStatuses);
170
+ }
171
+
172
+ export interface McpConfigValidationResult {
173
+ valid: boolean;
174
+ errors?: string[];
175
+ warnings?: string[];
176
+ }
177
+
178
+ export function validateMcpConfig(name: string, config: unknown): McpConfigValidationResult {
179
+ const errors: string[] = [];
180
+ const warnings: string[] = [];
181
+
182
+ if (!name || typeof name !== "string") {
183
+ errors.push("Server name is required");
184
+ } else if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
185
+ errors.push("Server name must contain only alphanumeric characters, hyphens, and underscores");
186
+ }
187
+
188
+ if (!config || typeof config !== "object") {
189
+ errors.push("Invalid MCP server configuration");
190
+ return { valid: false, errors, warnings };
191
+ }
192
+
193
+ const serverConfig = config as McpServerConfig;
194
+ if (serverConfig.command) {
195
+ // stdio config
196
+ } else if (serverConfig.url) {
197
+ try { new URL(serverConfig.url); }
198
+ catch { errors.push("Invalid URL format"); }
199
+ if (serverConfig.url.startsWith("http://") && !serverConfig.url.includes("localhost")) {
200
+ warnings.push("Using HTTP instead of HTTPS may expose credentials");
201
+ }
202
+ } else {
203
+ errors.push("Either command or url is required");
204
+ }
205
+
206
+ return { valid: errors.length === 0, errors: errors.length > 0 ? errors : undefined, warnings: warnings.length > 0 ? warnings : undefined };
207
+ }
208
+
209
+ export interface McpConfigExport {
210
+ version: number;
211
+ servers: Record<string, McpServerConfig>;
212
+ }
213
+
214
+ export function exportMcpConfigs(): McpConfigExport {
215
+ const servers: Record<string, McpServerConfig> = {};
216
+ for (const [name, scopedConfig] of mcpConfigs) {
217
+ const { scope: _scope, description: _desc, enabled: _enabled, ...config } = scopedConfig;
218
+ servers[name] = config;
219
+ }
220
+ return { version: 1, servers };
221
+ }
222
+
223
+ export function importMcpConfigs(exported: McpConfigExport): number {
224
+ let imported = 0;
225
+ for (const [name, config] of Object.entries(exported.servers)) {
226
+ if (config.command || config.url) {
227
+ setMcpConfig(name, config, "user");
228
+ imported++;
229
+ }
230
+ }
231
+ return imported;
232
+ }
@@ -0,0 +1,258 @@
1
+ /**
2
+ * MCP Connection Manager
3
+ */
4
+
5
+ import { connectToServer, disconnectFromServer, type ConnectResult } from "./client.js";
6
+ import type { MCPServerConnection, ConnectedMCPServer, FailedMCPServer, SerializedTool, ServerResource, ScopedMcpConfig } from "./types.js";
7
+ import { getAllMcpConfigs, isMcpServerDisabled, setMcpServerEnabled, loadMcpConfigs } from "./config.js";
8
+
9
+ interface ConnectionManagerState {
10
+ connections: Map<string, MCPServerConnection>;
11
+ isConnecting: boolean;
12
+ error: string | null;
13
+ }
14
+
15
+ let connectionManager: ConnectionManagerState = {
16
+ connections: new Map(),
17
+ isConnecting: false,
18
+ error: null,
19
+ };
20
+
21
+ export interface McpConnectionProgress {
22
+ total: number;
23
+ completed: number;
24
+ currentServer: string;
25
+ status: "connecting" | "connected" | "failed" | "disabled";
26
+ error?: string;
27
+ }
28
+
29
+ export type McpProgressCallback = (progress: McpConnectionProgress) => void;
30
+
31
+ export async function initializeMcpConnections(options?: { onProgress?: (message: string) => void; onProgressUpdate?: McpProgressCallback }): Promise<MCPServerConnection[]> {
32
+ loadMcpConfigs();
33
+ connectionManager.isConnecting = true;
34
+ connectionManager.error = null;
35
+
36
+ const configs = getAllMcpConfigs();
37
+ const connections: MCPServerConnection[] = [];
38
+ const total = configs.size;
39
+ let completed = 0;
40
+
41
+ // Separate disabled and active configs
42
+ const disabledServers: Array<{ name: string; config: ScopedMcpConfig }> = [];
43
+ const activeConfigs: Array<{ name: string; config: ScopedMcpConfig }> = [];
44
+
45
+ for (const [name, config] of configs) {
46
+ if (isMcpServerDisabled(name)) {
47
+ disabledServers.push({ name, config });
48
+ } else {
49
+ activeConfigs.push({ name, config });
50
+ }
51
+ }
52
+
53
+ // Set disabled servers immediately
54
+ for (const { name, config } of disabledServers) {
55
+ connectionManager.connections.set(name, { name, status: "disabled", config });
56
+ completed++;
57
+ options?.onProgress?.(`[${completed}/${total}] ${name} (disabled)`);
58
+ options?.onProgressUpdate?.({
59
+ total,
60
+ completed,
61
+ currentServer: name,
62
+ status: "disabled",
63
+ });
64
+ }
65
+
66
+ options?.onProgress?.(`Connecting to ${activeConfigs.length} MCP servers in parallel...`);
67
+
68
+ // Connect to active servers in parallel
69
+ const connectPromises = activeConfigs.map(async ({ name, config }) => {
70
+ options?.onProgress?.(`[${completed + 1}/${total}] Connecting to ${name}...`);
71
+ options?.onProgressUpdate?.({
72
+ total,
73
+ completed,
74
+ currentServer: name,
75
+ status: "connecting",
76
+ });
77
+
78
+ const result = await connectToServer(name, config, { onProgress: options?.onProgress });
79
+
80
+ completed++;
81
+
82
+ if (result.success && result.server) {
83
+ connectionManager.connections.set(name, result.server);
84
+ connections.push(result.server);
85
+ options?.onProgress?.(`[${completed}/${total}] ✓ ${name} connected`);
86
+ options?.onProgressUpdate?.({
87
+ total,
88
+ completed,
89
+ currentServer: name,
90
+ status: "connected",
91
+ });
92
+ } else {
93
+ const failedServer: FailedMCPServer = {
94
+ name,
95
+ status: "failed",
96
+ config,
97
+ error: result.error,
98
+ lastAttempt: Date.now(),
99
+ };
100
+ connectionManager.connections.set(name, failedServer);
101
+ options?.onProgress?.(`[${completed}/${total}] ✗ ${name} failed: ${result.error}`);
102
+ options?.onProgressUpdate?.({
103
+ total,
104
+ completed,
105
+ currentServer: name,
106
+ status: "failed",
107
+ error: result.error,
108
+ });
109
+ }
110
+ });
111
+
112
+ await Promise.all(connectPromises);
113
+
114
+ connectionManager.isConnecting = false;
115
+ return connections;
116
+ }
117
+
118
+ export async function connectMcpServer(name: string, options?: { timeout?: number; onProgress?: (message: string) => void }): Promise<ConnectResult> {
119
+ const configs = getAllMcpConfigs();
120
+ const config = configs.get(name);
121
+
122
+ if (!config) return { success: false, error: `Server '${name}' not found` };
123
+ if (isMcpServerDisabled(name)) return { success: false, error: `Server '${name}' is disabled` };
124
+
125
+ const result = await connectToServer(name, config, options);
126
+
127
+ if (result.success && result.server) {
128
+ connectionManager.connections.set(name, result.server);
129
+ }
130
+
131
+ return result;
132
+ }
133
+
134
+ export async function disconnectMcpServer(name: string): Promise<void> {
135
+ const connection = connectionManager.connections.get(name);
136
+ if (connection && connection.status === "connected") {
137
+ await disconnectFromServer(connection);
138
+ connectionManager.connections.delete(name);
139
+ }
140
+ }
141
+
142
+ export async function toggleMcpServer(name: string, enabled: boolean): Promise<void> {
143
+ const connection = connectionManager.connections.get(name);
144
+
145
+ if (enabled) {
146
+ // 启用服务器
147
+ setMcpServerEnabled(name, true);
148
+ if (connection && connection.status === "disabled") {
149
+ connectionManager.connections.set(name, {
150
+ ...connection,
151
+ status: "disconnected",
152
+ });
153
+ }
154
+ } else {
155
+ // 禁用服务器
156
+ setMcpServerEnabled(name, false);
157
+ if (connection && connection.status === "connected") {
158
+ await disconnectFromServer(connection);
159
+ }
160
+ connectionManager.connections.set(name, {
161
+ name,
162
+ status: "disabled",
163
+ config: connection?.config ?? getAllMcpConfigs().get(name)!,
164
+ });
165
+ }
166
+ }
167
+
168
+ export async function reconnectMcpServer(name: string, options?: { timeout?: number; onProgress?: (message: string) => void }): Promise<ConnectResult> {
169
+ await disconnectMcpServer(name);
170
+ setMcpServerEnabled(name, true);
171
+ const result = await connectMcpServer(name, options);
172
+ if (!result.success) setMcpServerEnabled(name, false);
173
+ return result;
174
+ }
175
+
176
+ export function getMcpConnections(): Map<string, MCPServerConnection> {
177
+ return new Map(connectionManager.connections);
178
+ }
179
+
180
+ export function getMcpConnection(name: string): MCPServerConnection | undefined {
181
+ return connectionManager.connections.get(name);
182
+ }
183
+
184
+ export function getConnectedServers(): ConnectedMCPServer[] {
185
+ return Array.from(connectionManager.connections.values()).filter(c => c.status === "connected") as ConnectedMCPServer[];
186
+ }
187
+
188
+ export function getAllMcpTools(): SerializedTool[] {
189
+ const tools: SerializedTool[] = [];
190
+ for (const connection of connectionManager.connections.values()) {
191
+ if (connection.status === "connected") {
192
+ for (const tool of connection.tools) {
193
+ tools.push({
194
+ name: `mcp__${connection.name}__${tool.name}`,
195
+ description: tool.description,
196
+ inputSchema: tool.inputSchema,
197
+ isMcp: true,
198
+ serverName: connection.name,
199
+ originalToolName: tool.name,
200
+ });
201
+ }
202
+ }
203
+ }
204
+ return tools;
205
+ }
206
+
207
+ export function getAllMcpResources(): ServerResource[] {
208
+ const resources: ServerResource[] = [];
209
+ for (const connection of connectionManager.connections.values()) {
210
+ if (connection.status === "connected") {
211
+ for (const resource of connection.resources) {
212
+ resources.push({ ...resource, server: connection.name });
213
+ }
214
+ }
215
+ }
216
+ return resources;
217
+ }
218
+
219
+ export function getMcpStatusSummary(): { total: number; connected: number; failed: number; disabled: number; pending: number } {
220
+ let connected = 0;
221
+ let failed = 0;
222
+ let disabled = 0;
223
+ let pending = 0;
224
+
225
+ for (const connection of connectionManager.connections.values()) {
226
+ switch (connection.status) {
227
+ case "connected": connected++; break;
228
+ case "failed": failed++; break;
229
+ case "disabled": disabled++; break;
230
+ case "pending": pending++; break;
231
+ }
232
+ }
233
+
234
+ return { total: connectionManager.connections.size, connected, failed, disabled, pending };
235
+ }
236
+
237
+ export function isMcpConnecting(): boolean {
238
+ return connectionManager.isConnecting;
239
+ }
240
+
241
+ export function getMcpError(): string | null {
242
+ return connectionManager.error;
243
+ }
244
+
245
+ export async function disconnectAllMcpServers(): Promise<void> {
246
+ const promises: Promise<void>[] = [];
247
+ for (const connection of connectionManager.connections.values()) {
248
+ if (connection.status === "connected") {
249
+ promises.push(disconnectFromServer(connection));
250
+ }
251
+ }
252
+ await Promise.all(promises);
253
+ connectionManager.connections.clear();
254
+ }
255
+
256
+ export function resetMcpConnectionManager(): void {
257
+ connectionManager = { connections: new Map(), isConnecting: false, error: null };
258
+ }