oricore 1.0.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/LICENSE +21 -0
- package/README.md +199 -0
- package/dist/agent/agent/agentManager.d.ts +38 -0
- package/dist/agent/agent/builtin/common.d.ts +5 -0
- package/dist/agent/agent/builtin/explore.d.ts +5 -0
- package/dist/agent/agent/builtin/general-purpose.d.ts +5 -0
- package/dist/agent/agent/builtin/index.d.ts +5 -0
- package/dist/agent/agent/executor.d.ts +2 -0
- package/dist/agent/agent/types.d.ts +98 -0
- package/dist/api/engine.d.ts +213 -0
- package/dist/communication/index.d.ts +4 -0
- package/dist/communication/messageBus.d.ts +71 -0
- package/dist/core/at.d.ts +26 -0
- package/dist/core/backgroundTaskManager.d.ts +27 -0
- package/dist/core/compact.d.ts +9 -0
- package/dist/core/config.d.ts +103 -0
- package/dist/core/constants.d.ts +32 -0
- package/dist/core/context.d.ts +57 -0
- package/dist/core/globalData.d.ts +21 -0
- package/dist/core/history.d.ts +24 -0
- package/dist/core/ide.d.ts +103 -0
- package/dist/core/jsonl.d.ts +37 -0
- package/dist/core/llmsContext.d.ts +14 -0
- package/dist/core/loop.d.ts +82 -0
- package/dist/core/message.d.ts +132 -0
- package/dist/core/model.d.ts +79 -0
- package/dist/core/output-style/builtin/default.d.ts +2 -0
- package/dist/core/output-style/builtin/explanatory.d.ts +2 -0
- package/dist/core/output-style/builtin/index.d.ts +6 -0
- package/dist/core/output-style/builtin/miao.d.ts +2 -0
- package/dist/core/output-style/builtin/minimal.d.ts +2 -0
- package/dist/core/output-style/types.d.ts +6 -0
- package/dist/core/outputFormat.d.ts +29 -0
- package/dist/core/outputStyle.d.ts +43 -0
- package/dist/core/paths.d.ts +20 -0
- package/dist/core/planSystemPrompt.d.ts +5 -0
- package/dist/core/plugin.d.ts +138 -0
- package/dist/core/project.d.ts +64 -0
- package/dist/core/promptCache.d.ts +3 -0
- package/dist/core/query.d.ts +14 -0
- package/dist/core/rules.d.ts +8 -0
- package/dist/core/systemPrompt.d.ts +9 -0
- package/dist/core/thinking-config.d.ts +3 -0
- package/dist/core/usage.d.ts +14 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +144432 -0
- package/dist/mcp/mcp.d.ts +49 -0
- package/dist/modes/builtin.d.ts +34 -0
- package/dist/modes/index.d.ts +8 -0
- package/dist/modes/registry.d.ts +18 -0
- package/dist/modes/types.d.ts +51 -0
- package/dist/platform/index.d.ts +5 -0
- package/dist/platform/node.d.ts +28 -0
- package/dist/platform/types.d.ts +41 -0
- package/dist/session/session.d.ts +43 -0
- package/dist/skill/skill.d.ts +79 -0
- package/dist/tools/tool.d.ts +119 -0
- package/dist/tools/tools/askUserQuestion.d.ts +48 -0
- package/dist/tools/tools/bash.d.ts +43 -0
- package/dist/tools/tools/edit.d.ts +9 -0
- package/dist/tools/tools/fetch.d.ts +9 -0
- package/dist/tools/tools/glob.d.ts +7 -0
- package/dist/tools/tools/grep.d.ts +22 -0
- package/dist/tools/tools/ls.d.ts +6 -0
- package/dist/tools/tools/read.d.ts +9 -0
- package/dist/tools/tools/skill.d.ts +7 -0
- package/dist/tools/tools/task.d.ts +14 -0
- package/dist/tools/tools/todo.d.ts +37 -0
- package/dist/tools/tools/write.d.ts +7 -0
- package/dist/utils/apiKeyRotation.d.ts +2 -0
- package/dist/utils/applyEdit.d.ts +17 -0
- package/dist/utils/background-detection.d.ts +2 -0
- package/dist/utils/dotenv.d.ts +9 -0
- package/dist/utils/env.d.ts +6 -0
- package/dist/utils/error.d.ts +11 -0
- package/dist/utils/execFileNoThrow.d.ts +8 -0
- package/dist/utils/files.d.ts +10 -0
- package/dist/utils/git.d.ts +163 -0
- package/dist/utils/ide.d.ts +27 -0
- package/dist/utils/ignore.d.ts +6 -0
- package/dist/utils/isLocal.d.ts +1 -0
- package/dist/utils/language.d.ts +9 -0
- package/dist/utils/list.d.ts +20 -0
- package/dist/utils/mergeSystemMessagesMiddleware.d.ts +2 -0
- package/dist/utils/messageNormalization.d.ts +22 -0
- package/dist/utils/path.d.ts +34 -0
- package/dist/utils/prependSystemMessageMiddleware.d.ts +2 -0
- package/dist/utils/project.d.ts +1 -0
- package/dist/utils/proxy.d.ts +18 -0
- package/dist/utils/randomUUID.d.ts +5 -0
- package/dist/utils/renderSessionMarkdown.d.ts +10 -0
- package/dist/utils/ripgrep.d.ts +16 -0
- package/dist/utils/safeFrontMatter.d.ts +11 -0
- package/dist/utils/safeParseJson.d.ts +1 -0
- package/dist/utils/safeStringify.d.ts +1 -0
- package/dist/utils/sanitizeAIResponse.d.ts +30 -0
- package/dist/utils/setTerminalTitle.d.ts +1 -0
- package/dist/utils/shell-execution.d.ts +44 -0
- package/dist/utils/string.d.ts +8 -0
- package/dist/utils/symbols.d.ts +14 -0
- package/dist/utils/system-encoding.d.ts +40 -0
- package/dist/utils/tokenCounter.d.ts +8 -0
- package/dist/utils/username.d.ts +1 -0
- package/package.json +106 -0
- package/src/agent/agent/agentManager.test.ts +124 -0
- package/src/agent/agent/agentManager.ts +372 -0
- package/src/agent/agent/builtin/common.ts +20 -0
- package/src/agent/agent/builtin/explore.ts +53 -0
- package/src/agent/agent/builtin/general-purpose.ts +38 -0
- package/src/agent/agent/builtin/index.ts +13 -0
- package/src/agent/agent/executor.test.ts +339 -0
- package/src/agent/agent/executor.ts +224 -0
- package/src/agent/agent/types.ts +119 -0
- package/src/api/engine.ts +466 -0
- package/src/communication/index.ts +18 -0
- package/src/communication/messageBus.ts +393 -0
- package/src/core/at.ts +315 -0
- package/src/core/backgroundTaskManager.ts +129 -0
- package/src/core/compact.ts +95 -0
- package/src/core/config.ts +441 -0
- package/src/core/constants.ts +82 -0
- package/src/core/context.ts +214 -0
- package/src/core/globalData.ts +77 -0
- package/src/core/history.ts +323 -0
- package/src/core/ide.ts +325 -0
- package/src/core/jsonl.ts +100 -0
- package/src/core/llmsContext.ts +117 -0
- package/src/core/loop.ts +638 -0
- package/src/core/message.ts +304 -0
- package/src/core/model.ts +2198 -0
- package/src/core/output-style/builtin/default.ts +9 -0
- package/src/core/output-style/builtin/explanatory.ts +22 -0
- package/src/core/output-style/builtin/index.ts +19 -0
- package/src/core/output-style/builtin/miao.ts +22 -0
- package/src/core/output-style/builtin/minimal.ts +8 -0
- package/src/core/output-style/types.ts +6 -0
- package/src/core/outputFormat.ts +93 -0
- package/src/core/outputStyle.ts +255 -0
- package/src/core/paths.ts +161 -0
- package/src/core/planSystemPrompt.ts +46 -0
- package/src/core/plugin.ts +299 -0
- package/src/core/project.ts +492 -0
- package/src/core/promptCache.ts +32 -0
- package/src/core/query.ts +46 -0
- package/src/core/rules.ts +56 -0
- package/src/core/systemPrompt.ts +176 -0
- package/src/core/thinking-config.ts +98 -0
- package/src/core/usage.ts +68 -0
- package/src/index.ts +39 -0
- package/src/mcp/mcp.ts +637 -0
- package/src/modes/builtin.ts +305 -0
- package/src/modes/index.ts +22 -0
- package/src/modes/registry.ts +39 -0
- package/src/modes/types.ts +56 -0
- package/src/platform/index.ts +6 -0
- package/src/platform/node.ts +108 -0
- package/src/platform/types.ts +54 -0
- package/src/plugins/index.ts +15 -0
- package/src/session/session.ts +187 -0
- package/src/skill/skill.ts +702 -0
- package/src/tools/tool.ts +378 -0
- package/src/tools/tools/askUserQuestion.ts +134 -0
- package/src/tools/tools/bash.test.ts +425 -0
- package/src/tools/tools/bash.ts +999 -0
- package/src/tools/tools/edit.ts +86 -0
- package/src/tools/tools/fetch.ts +129 -0
- package/src/tools/tools/glob.ts +69 -0
- package/src/tools/tools/grep.test.ts +194 -0
- package/src/tools/tools/grep.ts +358 -0
- package/src/tools/tools/ls.ts +51 -0
- package/src/tools/tools/read.test.ts +169 -0
- package/src/tools/tools/read.ts +284 -0
- package/src/tools/tools/skill.ts +73 -0
- package/src/tools/tools/task.test.ts +262 -0
- package/src/tools/tools/task.ts +284 -0
- package/src/tools/tools/todo.ts +269 -0
- package/src/tools/tools/write.ts +71 -0
- package/src/types.d.ts +18 -0
- package/src/utils/apiKeyRotation.test.ts +70 -0
- package/src/utils/apiKeyRotation.ts +24 -0
- package/src/utils/applyEdit.test.ts +388 -0
- package/src/utils/applyEdit.ts +547 -0
- package/src/utils/background-detection.test.ts +61 -0
- package/src/utils/background-detection.ts +58 -0
- package/src/utils/dotenv.ts +26 -0
- package/src/utils/env.ts +90 -0
- package/src/utils/error.ts +38 -0
- package/src/utils/execFileNoThrow.ts +49 -0
- package/src/utils/files.ts +93 -0
- package/src/utils/git.ts +1152 -0
- package/src/utils/ide.ts +279 -0
- package/src/utils/ignore.ts +275 -0
- package/src/utils/isLocal.ts +6 -0
- package/src/utils/language.ts +33 -0
- package/src/utils/list.ts +200 -0
- package/src/utils/mergeSystemMessagesMiddleware.ts +32 -0
- package/src/utils/messageNormalization.test.ts +401 -0
- package/src/utils/messageNormalization.ts +168 -0
- package/src/utils/path.ts +98 -0
- package/src/utils/prependSystemMessageMiddleware.ts +16 -0
- package/src/utils/project.ts +32 -0
- package/src/utils/proxy.ts +102 -0
- package/src/utils/randomUUID.ts +11 -0
- package/src/utils/renderSessionMarkdown.ts +175 -0
- package/src/utils/ripgrep.ts +189 -0
- package/src/utils/safeFrontMatter.test.ts +118 -0
- package/src/utils/safeFrontMatter.ts +68 -0
- package/src/utils/safeParseJson.ts +7 -0
- package/src/utils/safeStringify.ts +10 -0
- package/src/utils/sanitizeAIResponse.test.ts +135 -0
- package/src/utils/sanitizeAIResponse.ts +55 -0
- package/src/utils/setTerminalTitle.ts +7 -0
- package/src/utils/shell-execution.test.ts +237 -0
- package/src/utils/shell-execution.ts +279 -0
- package/src/utils/string.ts +13 -0
- package/src/utils/symbols.ts +18 -0
- package/src/utils/system-encoding.test.ts +164 -0
- package/src/utils/system-encoding.ts +296 -0
- package/src/utils/tokenCounter.test.ts +38 -0
- package/src/utils/tokenCounter.ts +19 -0
- package/src/utils/username.ts +21 -0
package/src/mcp/mcp.ts
ADDED
|
@@ -0,0 +1,637 @@
|
|
|
1
|
+
import { experimental_createMCPClient } from '@ai-sdk/mcp';
|
|
2
|
+
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
|
|
3
|
+
import createDebug from 'debug';
|
|
4
|
+
import { existsSync, readFileSync } from 'fs';
|
|
5
|
+
import { resolve } from 'pathe';
|
|
6
|
+
import type { ImagePart, TextPart } from '../core/message';
|
|
7
|
+
import type { Tool } from '../tools/tool';
|
|
8
|
+
import { safeStringify } from '../utils/safeStringify';
|
|
9
|
+
|
|
10
|
+
export interface MCPConfig {
|
|
11
|
+
type?: 'stdio' | 'sse' | 'http';
|
|
12
|
+
command?: string;
|
|
13
|
+
args?: string[];
|
|
14
|
+
env?: Record<string, string>;
|
|
15
|
+
url?: string;
|
|
16
|
+
disable?: boolean;
|
|
17
|
+
/**
|
|
18
|
+
* The timeout for tool calls in milliseconds.
|
|
19
|
+
*/
|
|
20
|
+
timeout?: number;
|
|
21
|
+
headers?: Record<string, string>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const debug = createDebug('oricore:mcp');
|
|
25
|
+
|
|
26
|
+
type MCPServerStatus =
|
|
27
|
+
| 'pending'
|
|
28
|
+
| 'connecting'
|
|
29
|
+
| 'connected'
|
|
30
|
+
| 'failed'
|
|
31
|
+
| 'disconnected';
|
|
32
|
+
|
|
33
|
+
interface ServerState {
|
|
34
|
+
config: MCPConfig;
|
|
35
|
+
status: MCPServerStatus;
|
|
36
|
+
error?: string;
|
|
37
|
+
tools?: Record<string, any>;
|
|
38
|
+
client?: any; // Store client for cleanup
|
|
39
|
+
retryCount: number;
|
|
40
|
+
isTemporaryError?: boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export class MCPManager {
|
|
44
|
+
private servers: Map<string, ServerState> = new Map();
|
|
45
|
+
private configs: Record<string, MCPConfig> = {};
|
|
46
|
+
private isInitialized: boolean = false;
|
|
47
|
+
private initPromise?: Promise<void>;
|
|
48
|
+
private initLock: boolean = false;
|
|
49
|
+
|
|
50
|
+
static create(mcpServers: Record<string, MCPConfig>): MCPManager {
|
|
51
|
+
debug('create MCPManager', mcpServers);
|
|
52
|
+
const manager = new MCPManager();
|
|
53
|
+
manager.configs = mcpServers || {};
|
|
54
|
+
|
|
55
|
+
// Initialize servers state without connecting
|
|
56
|
+
for (const [key, config] of Object.entries(mcpServers || {})) {
|
|
57
|
+
if (config.disable) {
|
|
58
|
+
debug(`Skipping disabled MCP server: ${key}`);
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
manager.servers.set(key, {
|
|
62
|
+
config,
|
|
63
|
+
status: 'pending',
|
|
64
|
+
retryCount: 0,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return manager;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async initAsync(): Promise<void> {
|
|
72
|
+
// Return existing promise if initialization is already in progress
|
|
73
|
+
if (this.initPromise) {
|
|
74
|
+
return this.initPromise;
|
|
75
|
+
}
|
|
76
|
+
// Double-check locking pattern for thread safety
|
|
77
|
+
if (this.initLock) {
|
|
78
|
+
// Wait for lock to be released and check if initialization completed
|
|
79
|
+
while (this.initLock) {
|
|
80
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
81
|
+
}
|
|
82
|
+
if (this.isInitialized) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// Acquire lock
|
|
87
|
+
this.initLock = true;
|
|
88
|
+
try {
|
|
89
|
+
// Check again in case another thread completed initialization
|
|
90
|
+
if (this.isInitialized) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
this.initPromise = this._performInit();
|
|
94
|
+
await this.initPromise;
|
|
95
|
+
} finally {
|
|
96
|
+
// Release lock
|
|
97
|
+
this.initLock = false;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
private async _performInit(): Promise<void> {
|
|
102
|
+
debug('Starting async MCP initialization');
|
|
103
|
+
const connectionPromises: Promise<void>[] = [];
|
|
104
|
+
|
|
105
|
+
for (const [key, config] of Object.entries(this.configs)) {
|
|
106
|
+
if (config.disable) {
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const connectionPromise = this._connectServer(key, config);
|
|
111
|
+
connectionPromises.push(connectionPromise);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Wait for all connections to complete (success or failure)
|
|
115
|
+
await Promise.allSettled(connectionPromises);
|
|
116
|
+
this.isInitialized = true;
|
|
117
|
+
debug('MCP initialization completed');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private async _connectServer(key: string, config: MCPConfig): Promise<void> {
|
|
121
|
+
const serverState = this.servers.get(key);
|
|
122
|
+
if (!serverState) return;
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
debug(`Connecting MCP server: ${key}`);
|
|
126
|
+
serverState.status = 'connecting';
|
|
127
|
+
|
|
128
|
+
// Test connection and fetch tools
|
|
129
|
+
const { client, tools } = await this._testConnectionAndFetchTools(config);
|
|
130
|
+
|
|
131
|
+
serverState.status = 'connected';
|
|
132
|
+
serverState.client = client;
|
|
133
|
+
serverState.tools = tools;
|
|
134
|
+
serverState.error = undefined;
|
|
135
|
+
|
|
136
|
+
debug(
|
|
137
|
+
`MCP server connected successfully: ${key}, tools: ${Object.keys(tools).length}`,
|
|
138
|
+
);
|
|
139
|
+
} catch (error) {
|
|
140
|
+
const errorMessage =
|
|
141
|
+
error instanceof Error ? error.message : String(error);
|
|
142
|
+
debug(`Failed to connect MCP server ${key}: ${errorMessage}`);
|
|
143
|
+
|
|
144
|
+
// Classify error types for better handling
|
|
145
|
+
const isTemporaryError = this._isTemporaryError(error);
|
|
146
|
+
|
|
147
|
+
serverState.status = 'failed';
|
|
148
|
+
serverState.error = errorMessage;
|
|
149
|
+
serverState.retryCount += 1;
|
|
150
|
+
serverState.isTemporaryError = isTemporaryError;
|
|
151
|
+
|
|
152
|
+
// Ensure no client reference is left on failure
|
|
153
|
+
serverState.client = undefined;
|
|
154
|
+
serverState.tools = undefined;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async getAllTools(): Promise<Tool[]> {
|
|
159
|
+
const allTools: Tool[] = [];
|
|
160
|
+
const toolNames = new Set<string>();
|
|
161
|
+
|
|
162
|
+
for (const [serverName, serverState] of this.servers.entries()) {
|
|
163
|
+
if (serverState.status !== 'connected' || !serverState.tools) {
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
for (const [toolName, toolDef] of Object.entries(serverState.tools)) {
|
|
168
|
+
const fullToolName = `mcp__${serverName}__${toolName}`;
|
|
169
|
+
|
|
170
|
+
if (toolNames.has(fullToolName)) {
|
|
171
|
+
throw new Error(`Duplicate tool name found: ${fullToolName}`);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
toolNames.add(fullToolName);
|
|
175
|
+
allTools.push(
|
|
176
|
+
this.#convertAiSdkToolToLocal(
|
|
177
|
+
toolName,
|
|
178
|
+
toolDef,
|
|
179
|
+
serverName,
|
|
180
|
+
serverState.config,
|
|
181
|
+
),
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return allTools;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async getTools(keys: string[]): Promise<Tool[]> {
|
|
190
|
+
const allTools: Tool[] = [];
|
|
191
|
+
const toolNames = new Set<string>();
|
|
192
|
+
|
|
193
|
+
for (const key of keys) {
|
|
194
|
+
const serverState = this.servers.get(key);
|
|
195
|
+
if (
|
|
196
|
+
!serverState ||
|
|
197
|
+
serverState.status !== 'connected' ||
|
|
198
|
+
!serverState.tools
|
|
199
|
+
) {
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
for (const [toolName, toolDef] of Object.entries(serverState.tools)) {
|
|
204
|
+
const fullToolName = `mcp__${key}__${toolName}`;
|
|
205
|
+
|
|
206
|
+
if (toolNames.has(fullToolName)) {
|
|
207
|
+
throw new Error(`Duplicate tool name found: ${fullToolName}`);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
toolNames.add(fullToolName);
|
|
211
|
+
allTools.push(
|
|
212
|
+
this.#convertAiSdkToolToLocal(
|
|
213
|
+
toolName,
|
|
214
|
+
toolDef,
|
|
215
|
+
key,
|
|
216
|
+
serverState.config,
|
|
217
|
+
),
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return allTools;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async destroy() {
|
|
226
|
+
// Close all client connections
|
|
227
|
+
const closePromises = Array.from(this.servers.values())
|
|
228
|
+
.filter((state) => state.client)
|
|
229
|
+
.map((state) =>
|
|
230
|
+
state.client.close().catch((err: Error) => {
|
|
231
|
+
debug('Error closing client during destroy:', err);
|
|
232
|
+
}),
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
await Promise.allSettled(closePromises);
|
|
236
|
+
this.servers.clear();
|
|
237
|
+
this.isInitialized = false;
|
|
238
|
+
this.initPromise = undefined;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
getServerNames(): string[] {
|
|
242
|
+
return Array.from(this.servers.keys());
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
hasServer(name: string): boolean {
|
|
246
|
+
return this.servers.has(name);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
getServerStatus(name: string): MCPServerStatus | undefined {
|
|
250
|
+
return this.servers.get(name)?.status;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
getServerError(name: string): string | undefined {
|
|
254
|
+
return this.servers.get(name)?.error;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
async getAllServerStatus(): Promise<
|
|
258
|
+
Record<
|
|
259
|
+
string,
|
|
260
|
+
{ status: MCPServerStatus; error?: string; toolCount: number }
|
|
261
|
+
>
|
|
262
|
+
> {
|
|
263
|
+
await this.initAsync();
|
|
264
|
+
|
|
265
|
+
const result: Record<
|
|
266
|
+
string,
|
|
267
|
+
{ status: MCPServerStatus; error?: string; toolCount: number }
|
|
268
|
+
> = {};
|
|
269
|
+
for (const [name, state] of this.servers.entries()) {
|
|
270
|
+
result[name] = {
|
|
271
|
+
status: state.status,
|
|
272
|
+
error: state.error,
|
|
273
|
+
toolCount: state.tools ? Object.keys(state.tools).length : 0,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
return result;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
isReady(): boolean {
|
|
280
|
+
return this.isInitialized;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
isLoading(): boolean {
|
|
284
|
+
return !!this.initPromise && !this.isInitialized;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
async retryConnection(serverName: string): Promise<void> {
|
|
288
|
+
const config = this.configs[serverName];
|
|
289
|
+
if (!config) {
|
|
290
|
+
throw new Error(`Server ${serverName} not found in configuration`);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const serverState = this.servers.get(serverName);
|
|
294
|
+
if (!serverState) {
|
|
295
|
+
throw new Error(`Server ${serverName} state not found`);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Log reconnection attempt
|
|
299
|
+
debug(`Attempting to reconnect MCP server: ${serverName}`);
|
|
300
|
+
|
|
301
|
+
// Close existing client if any
|
|
302
|
+
if (serverState.client) {
|
|
303
|
+
try {
|
|
304
|
+
await serverState.client.close();
|
|
305
|
+
} catch (error) {
|
|
306
|
+
debug(`Error closing existing client for ${serverName}:`, error);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Reset state and retry
|
|
311
|
+
serverState.client = undefined;
|
|
312
|
+
serverState.tools = undefined;
|
|
313
|
+
serverState.error = undefined;
|
|
314
|
+
serverState.status = 'connecting';
|
|
315
|
+
|
|
316
|
+
await this._connectServer(serverName, config);
|
|
317
|
+
|
|
318
|
+
// Verify reconnection result
|
|
319
|
+
const newState = this.servers.get(serverName);
|
|
320
|
+
if (newState?.status !== 'connected') {
|
|
321
|
+
throw new Error(newState?.error || 'Reconnection failed');
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
debug(`Successfully reconnected MCP server: ${serverName}`);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
private async _createClient(config: MCPConfig) {
|
|
328
|
+
if (config.command) {
|
|
329
|
+
// Stdio transport (for local servers only)
|
|
330
|
+
const env = config.env
|
|
331
|
+
? { ...config.env, PATH: process.env.PATH || '' }
|
|
332
|
+
: undefined;
|
|
333
|
+
|
|
334
|
+
const { Experimental_StdioMCPTransport } = await import(
|
|
335
|
+
'@ai-sdk/mcp/mcp-stdio'
|
|
336
|
+
);
|
|
337
|
+
|
|
338
|
+
return experimental_createMCPClient({
|
|
339
|
+
transport: new Experimental_StdioMCPTransport({
|
|
340
|
+
command: config.command,
|
|
341
|
+
args: config.args,
|
|
342
|
+
stderr: 'ignore',
|
|
343
|
+
env,
|
|
344
|
+
}),
|
|
345
|
+
});
|
|
346
|
+
} else if (config.url) {
|
|
347
|
+
// HTTP or SSE transport
|
|
348
|
+
const transportType = config.type || 'http'; // Default to HTTP
|
|
349
|
+
if (transportType === 'sse') {
|
|
350
|
+
// SSE transport
|
|
351
|
+
return experimental_createMCPClient({
|
|
352
|
+
transport: {
|
|
353
|
+
type: 'sse',
|
|
354
|
+
url: config.url,
|
|
355
|
+
headers: config.headers,
|
|
356
|
+
},
|
|
357
|
+
});
|
|
358
|
+
} else {
|
|
359
|
+
// HTTP transport
|
|
360
|
+
return experimental_createMCPClient({
|
|
361
|
+
transport: new StreamableHTTPClientTransport(new URL(config.url), {
|
|
362
|
+
requestInit: {
|
|
363
|
+
headers: config.headers,
|
|
364
|
+
},
|
|
365
|
+
}),
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
} else {
|
|
369
|
+
throw new Error('MCP config must have either command or url configured');
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
private async _testConnectionAndFetchTools(
|
|
374
|
+
config: MCPConfig,
|
|
375
|
+
): Promise<{ client: any; tools: Record<string, any> }> {
|
|
376
|
+
const client = await this._createClient(config);
|
|
377
|
+
try {
|
|
378
|
+
const tools = await client.tools();
|
|
379
|
+
return { client, tools };
|
|
380
|
+
} catch (error) {
|
|
381
|
+
// Close client on error
|
|
382
|
+
await client.close().catch((err) => {
|
|
383
|
+
debug('Error closing client after connection failure:', err);
|
|
384
|
+
});
|
|
385
|
+
throw error;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
private _isTemporaryError(error: unknown): boolean {
|
|
390
|
+
if (!(error instanceof Error)) {
|
|
391
|
+
return false;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const message = error.message.toLowerCase();
|
|
395
|
+
|
|
396
|
+
// Network-related temporary errors
|
|
397
|
+
const temporaryErrors = [
|
|
398
|
+
'timeout',
|
|
399
|
+
'connection refused',
|
|
400
|
+
'network error',
|
|
401
|
+
'temporary',
|
|
402
|
+
'try again',
|
|
403
|
+
'rate limit',
|
|
404
|
+
'too many requests',
|
|
405
|
+
'service unavailable',
|
|
406
|
+
'socket hang up',
|
|
407
|
+
'econnreset',
|
|
408
|
+
'enotfound',
|
|
409
|
+
'econnrefused',
|
|
410
|
+
'etimedout',
|
|
411
|
+
];
|
|
412
|
+
|
|
413
|
+
// Configuration or permanent errors
|
|
414
|
+
const permanentErrors = [
|
|
415
|
+
'command not found',
|
|
416
|
+
'no such file',
|
|
417
|
+
'permission denied',
|
|
418
|
+
'invalid configuration',
|
|
419
|
+
'malformed',
|
|
420
|
+
'syntax error',
|
|
421
|
+
'authentication failed',
|
|
422
|
+
'unauthorized',
|
|
423
|
+
];
|
|
424
|
+
|
|
425
|
+
// Check for permanent errors first (higher priority)
|
|
426
|
+
if (permanentErrors.some((permanent) => message.includes(permanent))) {
|
|
427
|
+
return false;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// Check for temporary errors
|
|
431
|
+
if (temporaryErrors.some((temporary) => message.includes(temporary))) {
|
|
432
|
+
return true;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Default to temporary for unknown errors (safer for retries)
|
|
436
|
+
return true;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
#convertAiSdkToolToLocal(
|
|
440
|
+
toolName: string,
|
|
441
|
+
toolDef: any,
|
|
442
|
+
serverName: string,
|
|
443
|
+
config: MCPConfig,
|
|
444
|
+
): Tool {
|
|
445
|
+
return {
|
|
446
|
+
name: `mcp__${serverName.replace(/[^a-zA-Z0-9_-]/g, '')}__${toolName}`,
|
|
447
|
+
description: toolDef.description,
|
|
448
|
+
getDescription: ({ params }) => {
|
|
449
|
+
return formatParamsDescription(params as Record<string, any>);
|
|
450
|
+
},
|
|
451
|
+
// Why? Some models do not support null values, so null values need to be removed.
|
|
452
|
+
parameters: removeNullValues(toolDef.inputSchema.jsonSchema),
|
|
453
|
+
execute: async (params) => {
|
|
454
|
+
try {
|
|
455
|
+
// toolDef is already a Tool from AI SDK with an execute method
|
|
456
|
+
const result = await toolDef.execute(params || {});
|
|
457
|
+
|
|
458
|
+
const returnDisplay = `Tool ${toolName} executed successfully${params ? `, parameters: ${JSON.stringify(params)}` : ''}`;
|
|
459
|
+
const llmContent = convertMcpResultToLlmContent(result);
|
|
460
|
+
|
|
461
|
+
return {
|
|
462
|
+
llmContent,
|
|
463
|
+
returnDisplay,
|
|
464
|
+
};
|
|
465
|
+
} catch (error) {
|
|
466
|
+
return {
|
|
467
|
+
isError: true,
|
|
468
|
+
llmContent: error instanceof Error ? error.message : String(error),
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
},
|
|
472
|
+
approval: {
|
|
473
|
+
category: 'network',
|
|
474
|
+
},
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
export function parseMcpConfig(
|
|
480
|
+
mcpConfigArgs: string[],
|
|
481
|
+
cwd: string,
|
|
482
|
+
): Record<string, MCPConfig> {
|
|
483
|
+
const mcpServers: Record<string, MCPConfig> = {};
|
|
484
|
+
for (const configItem of mcpConfigArgs) {
|
|
485
|
+
let configData: unknown;
|
|
486
|
+
try {
|
|
487
|
+
// Try to parse as JSON string first
|
|
488
|
+
configData = JSON.parse(configItem);
|
|
489
|
+
} catch (e) {
|
|
490
|
+
// If JSON parsing fails, treat as file path
|
|
491
|
+
const configPath = resolve(cwd, configItem);
|
|
492
|
+
if (!existsSync(configPath)) {
|
|
493
|
+
throw new Error(`MCP config file not found: ${configPath}`);
|
|
494
|
+
}
|
|
495
|
+
try {
|
|
496
|
+
const fileContent = readFileSync(configPath, 'utf-8');
|
|
497
|
+
configData = JSON.parse(fileContent);
|
|
498
|
+
} catch (error) {
|
|
499
|
+
throw new Error(
|
|
500
|
+
`Failed to parse MCP config file ${configPath}: ${error instanceof Error ? error.message : String(error)}`,
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
// Extract mcpServer object from the config data
|
|
505
|
+
if (!configData || typeof configData !== 'object') {
|
|
506
|
+
throw new Error('MCP config must be a valid JSON object');
|
|
507
|
+
}
|
|
508
|
+
const configObj = configData as Record<string, unknown>;
|
|
509
|
+
if (!configObj.mcpServers || typeof configObj.mcpServers !== 'object') {
|
|
510
|
+
throw new Error('MCP config must contain an "mcpServers" object');
|
|
511
|
+
}
|
|
512
|
+
Object.assign(
|
|
513
|
+
mcpServers,
|
|
514
|
+
configObj.mcpServers as Record<string, MCPConfig>,
|
|
515
|
+
);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
return mcpServers;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function removeNullValues(obj: unknown): any {
|
|
522
|
+
if (obj === null || obj === undefined) {
|
|
523
|
+
return undefined;
|
|
524
|
+
}
|
|
525
|
+
if (Array.isArray(obj)) {
|
|
526
|
+
return obj.map(removeNullValues).filter((v) => v !== undefined);
|
|
527
|
+
}
|
|
528
|
+
if (typeof obj === 'object') {
|
|
529
|
+
const result: Record<string, unknown> = {};
|
|
530
|
+
for (const [k, v] of Object.entries(obj as Record<string, unknown>)) {
|
|
531
|
+
const cleaned = removeNullValues(v);
|
|
532
|
+
if (cleaned !== undefined) {
|
|
533
|
+
result[k] = cleaned;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
return result;
|
|
537
|
+
}
|
|
538
|
+
return obj;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function formatParamsDescription(params: Record<string, any>): string {
|
|
542
|
+
if (!params || typeof params !== 'object') {
|
|
543
|
+
return '';
|
|
544
|
+
}
|
|
545
|
+
const entries = Object.entries(params);
|
|
546
|
+
if (entries.length === 0) {
|
|
547
|
+
return '';
|
|
548
|
+
}
|
|
549
|
+
return entries
|
|
550
|
+
.filter(([key, value]) => value !== null && value !== undefined)
|
|
551
|
+
.map(([key, value]) => {
|
|
552
|
+
return `${key}: ${safeStringify(value)}`;
|
|
553
|
+
})
|
|
554
|
+
.join(', ');
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
export function convertMcpResultToLlmContent(
|
|
558
|
+
result: any,
|
|
559
|
+
): string | (TextPart | ImagePart)[] {
|
|
560
|
+
// Support mcp spec data types
|
|
561
|
+
// ref: https://modelcontextprotocol.io/specification/2025-06-18/server/tools#data-types
|
|
562
|
+
|
|
563
|
+
// Step 1: Unpack MCP result format
|
|
564
|
+
let actualContent: any;
|
|
565
|
+
|
|
566
|
+
if (result && typeof result === 'object' && !Array.isArray(result)) {
|
|
567
|
+
// why? https://github.com/vercel/ai/blob/main/packages/mcp/src/tool/types.ts#L201
|
|
568
|
+
if ('content' in result && Array.isArray(result.content)) {
|
|
569
|
+
// Format 1: { content: [...], isError?: boolean }
|
|
570
|
+
actualContent = result.content;
|
|
571
|
+
} else if ('toolResult' in result) {
|
|
572
|
+
// Format 2: { toolResult: unknown }
|
|
573
|
+
return safeStringify(result.toolResult);
|
|
574
|
+
} else {
|
|
575
|
+
// Fallback: treat as regular object
|
|
576
|
+
actualContent = result;
|
|
577
|
+
}
|
|
578
|
+
} else {
|
|
579
|
+
actualContent = result;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// Step 2: Type detection functions
|
|
583
|
+
const isTextPart = (part: object) => {
|
|
584
|
+
return 'type' in part && part.type === 'text' && 'text' in part;
|
|
585
|
+
};
|
|
586
|
+
const isImagePart = (part: object) => {
|
|
587
|
+
return (
|
|
588
|
+
'type' in part &&
|
|
589
|
+
part.type === 'image' &&
|
|
590
|
+
'data' in part &&
|
|
591
|
+
'mimeType' in part
|
|
592
|
+
);
|
|
593
|
+
};
|
|
594
|
+
const isResourcePart = (part: object) => {
|
|
595
|
+
return 'type' in part && part.type === 'resource' && 'resource' in part;
|
|
596
|
+
};
|
|
597
|
+
const isPart = (part: object) => {
|
|
598
|
+
return isTextPart(part) || isImagePart(part) || isResourcePart(part);
|
|
599
|
+
};
|
|
600
|
+
|
|
601
|
+
// Step 3: Process actualContent based on type
|
|
602
|
+
let llmContent: any = actualContent;
|
|
603
|
+
|
|
604
|
+
if (typeof llmContent === 'object' && !Array.isArray(llmContent)) {
|
|
605
|
+
// Single object: wrap in array if it's a part, otherwise stringify
|
|
606
|
+
if (isPart(llmContent as object)) {
|
|
607
|
+
llmContent = [llmContent];
|
|
608
|
+
} else {
|
|
609
|
+
llmContent = safeStringify(llmContent);
|
|
610
|
+
}
|
|
611
|
+
} else if (Array.isArray(llmContent)) {
|
|
612
|
+
// Array: check if it contains parts
|
|
613
|
+
const hasPart = llmContent.some(
|
|
614
|
+
(item) => typeof item === 'object' && item !== null && isPart(item),
|
|
615
|
+
);
|
|
616
|
+
if (hasPart) {
|
|
617
|
+
// Mixed array: convert non-part elements to text parts
|
|
618
|
+
llmContent = llmContent.map((part) => {
|
|
619
|
+
if (typeof part === 'object' && part !== null && isPart(part)) {
|
|
620
|
+
return part;
|
|
621
|
+
} else {
|
|
622
|
+
return { type: 'text', text: safeStringify(part) };
|
|
623
|
+
}
|
|
624
|
+
});
|
|
625
|
+
} else {
|
|
626
|
+
// Pure data array: stringify
|
|
627
|
+
llmContent = safeStringify(llmContent);
|
|
628
|
+
}
|
|
629
|
+
} else if (typeof llmContent === 'string') {
|
|
630
|
+
// Keep llmContent as string
|
|
631
|
+
} else {
|
|
632
|
+
// Other types: convert to string
|
|
633
|
+
llmContent = String(llmContent);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
return llmContent;
|
|
637
|
+
}
|