orquesta-cli 0.2.108 → 0.2.112
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/dist/cli.js +23 -1
- package/dist/core/commands/help.js +5 -0
- package/dist/core/commands/index.js +7 -0
- package/dist/core/commands/lsp.d.ts +3 -0
- package/dist/core/commands/lsp.js +37 -0
- package/dist/core/commands/mcp.d.ts +3 -0
- package/dist/core/commands/mcp.js +46 -0
- package/dist/core/commands/undo.d.ts +4 -0
- package/dist/core/commands/undo.js +45 -0
- package/dist/core/config/config-manager.d.ts +7 -1
- package/dist/core/config/config-manager.js +36 -0
- package/dist/core/file-snapshot-store.d.ts +25 -0
- package/dist/core/file-snapshot-store.js +104 -0
- package/dist/core/lsp/index.d.ts +6 -0
- package/dist/core/lsp/index.js +75 -0
- package/dist/core/lsp/jsonrpc.d.ts +18 -0
- package/dist/core/lsp/jsonrpc.js +38 -0
- package/dist/core/lsp/lsp-client.d.ts +40 -0
- package/dist/core/lsp/lsp-client.js +201 -0
- package/dist/core/lsp/server-registry.d.ts +14 -0
- package/dist/core/lsp/server-registry.js +85 -0
- package/dist/eval/eval-runner.js +14 -0
- package/dist/orchestration/plan-executor.js +2 -0
- package/dist/tools/llm/simple/file-tools.js +8 -2
- package/dist/tools/mcp/index.d.ts +3 -0
- package/dist/tools/mcp/index.js +3 -0
- package/dist/tools/mcp/mcp-client.d.ts +16 -0
- package/dist/tools/mcp/mcp-client.js +180 -0
- package/dist/tools/mcp/mcp-config.d.ts +4 -0
- package/dist/tools/mcp/mcp-config.js +87 -0
- package/dist/types/index.d.ts +21 -0
- package/dist/ui/components/PlanExecuteApp.js +25 -2
- package/dist/ui/hooks/slashCommandProcessor.js +17 -0
- package/package.json +2 -1
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { pathToFileURL, fileURLToPath } from 'url';
|
|
4
|
+
import { encodeMessage, MessageDecoder } from './jsonrpc.js';
|
|
5
|
+
import { logger } from '../../utils/logger.js';
|
|
6
|
+
const INIT_TIMEOUT_MS = Number(process.env['ORQUESTA_LSP_INIT_TIMEOUT_MS']) || 12000;
|
|
7
|
+
const DIAGNOSTICS_WAIT_MS = Number(process.env['ORQUESTA_LSP_DIAG_TIMEOUT_MS']) || 2500;
|
|
8
|
+
class LspClient {
|
|
9
|
+
def;
|
|
10
|
+
rootDir;
|
|
11
|
+
proc = null;
|
|
12
|
+
decoder = new MessageDecoder();
|
|
13
|
+
nextId = 1;
|
|
14
|
+
pendingRequests = new Map();
|
|
15
|
+
diagnostics = new Map();
|
|
16
|
+
diagWaiters = new Map();
|
|
17
|
+
openDocs = new Set();
|
|
18
|
+
initialized = false;
|
|
19
|
+
version = new Map();
|
|
20
|
+
constructor(def, rootDir) {
|
|
21
|
+
this.def = def;
|
|
22
|
+
this.rootDir = rootDir;
|
|
23
|
+
}
|
|
24
|
+
send(msg) {
|
|
25
|
+
if (!this.proc)
|
|
26
|
+
return;
|
|
27
|
+
this.proc.stdin.write(encodeMessage({ jsonrpc: '2.0', ...msg }));
|
|
28
|
+
}
|
|
29
|
+
request(method, params) {
|
|
30
|
+
const id = this.nextId++;
|
|
31
|
+
return new Promise((resolve, reject) => {
|
|
32
|
+
this.pendingRequests.set(id, { resolve: resolve, reject });
|
|
33
|
+
this.send({ id, method, params });
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
notify(method, params) {
|
|
37
|
+
this.send({ method, params });
|
|
38
|
+
}
|
|
39
|
+
handleMessage(msg) {
|
|
40
|
+
if (msg.id !== undefined && (msg.result !== undefined || msg.error !== undefined)) {
|
|
41
|
+
const pending = this.pendingRequests.get(msg.id);
|
|
42
|
+
if (pending) {
|
|
43
|
+
this.pendingRequests.delete(msg.id);
|
|
44
|
+
if (msg.error)
|
|
45
|
+
pending.reject(new Error(msg.error.message));
|
|
46
|
+
else
|
|
47
|
+
pending.resolve(msg.result);
|
|
48
|
+
}
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (msg.method === 'textDocument/publishDiagnostics') {
|
|
52
|
+
const params = msg.params;
|
|
53
|
+
const diags = (params.diagnostics || []).map((d) => ({
|
|
54
|
+
severity: d.severity ?? 1,
|
|
55
|
+
line: d.range?.start?.line ?? 0,
|
|
56
|
+
character: d.range?.start?.character ?? 0,
|
|
57
|
+
message: d.message ?? '',
|
|
58
|
+
source: d.source,
|
|
59
|
+
}));
|
|
60
|
+
this.diagnostics.set(params.uri, diags);
|
|
61
|
+
const waiters = this.diagWaiters.get(params.uri);
|
|
62
|
+
if (waiters) {
|
|
63
|
+
this.diagWaiters.delete(params.uri);
|
|
64
|
+
for (const w of waiters)
|
|
65
|
+
w();
|
|
66
|
+
}
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (msg.id !== undefined && msg.method) {
|
|
70
|
+
this.send({ id: msg.id, result: null });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
async start() {
|
|
74
|
+
if (this.initialized)
|
|
75
|
+
return;
|
|
76
|
+
if (this.proc) {
|
|
77
|
+
await this.waitFor(() => this.initialized, INIT_TIMEOUT_MS);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
logger.flow('LSP starting server', { id: this.def.id, root: this.rootDir });
|
|
81
|
+
const proc = spawn(this.def.command, this.def.args, {
|
|
82
|
+
cwd: this.rootDir,
|
|
83
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
84
|
+
env: process.env,
|
|
85
|
+
});
|
|
86
|
+
this.proc = proc;
|
|
87
|
+
proc.stdout.on('data', (chunk) => {
|
|
88
|
+
for (const msg of this.decoder.push(chunk))
|
|
89
|
+
this.handleMessage(msg);
|
|
90
|
+
});
|
|
91
|
+
proc.stderr.on('data', () => {
|
|
92
|
+
});
|
|
93
|
+
proc.on('error', (err) => {
|
|
94
|
+
logger.warn('LSP server process error', { id: this.def.id, error: err.message });
|
|
95
|
+
this.teardown();
|
|
96
|
+
});
|
|
97
|
+
proc.on('exit', () => this.teardown());
|
|
98
|
+
await this.request('initialize', {
|
|
99
|
+
processId: process.pid,
|
|
100
|
+
rootUri: pathToFileURL(this.rootDir).toString(),
|
|
101
|
+
rootPath: this.rootDir,
|
|
102
|
+
capabilities: {
|
|
103
|
+
textDocument: {
|
|
104
|
+
synchronization: { didSave: true, dynamicRegistration: false },
|
|
105
|
+
publishDiagnostics: { relatedInformation: false },
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
workspaceFolders: [{ uri: pathToFileURL(this.rootDir).toString(), name: path.basename(this.rootDir) }],
|
|
109
|
+
});
|
|
110
|
+
this.notify('initialized', {});
|
|
111
|
+
this.initialized = true;
|
|
112
|
+
logger.flow('LSP server initialized', { id: this.def.id });
|
|
113
|
+
}
|
|
114
|
+
async getDiagnostics(absPath, text) {
|
|
115
|
+
await this.start();
|
|
116
|
+
const uri = pathToFileURL(absPath).toString();
|
|
117
|
+
this.diagnostics.delete(uri);
|
|
118
|
+
const arrived = this.onceDiagnostics(uri);
|
|
119
|
+
if (!this.openDocs.has(uri)) {
|
|
120
|
+
this.openDocs.add(uri);
|
|
121
|
+
this.version.set(uri, 1);
|
|
122
|
+
this.notify('textDocument/didOpen', {
|
|
123
|
+
textDocument: { uri, languageId: this.def.id, version: 1, text },
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
const v = (this.version.get(uri) ?? 1) + 1;
|
|
128
|
+
this.version.set(uri, v);
|
|
129
|
+
this.notify('textDocument/didChange', {
|
|
130
|
+
textDocument: { uri, version: v },
|
|
131
|
+
contentChanges: [{ text }],
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
this.notify('textDocument/didSave', { textDocument: { uri }, text });
|
|
135
|
+
await Promise.race([arrived, this.delay(DIAGNOSTICS_WAIT_MS)]);
|
|
136
|
+
return this.diagnostics.get(uri) ?? [];
|
|
137
|
+
}
|
|
138
|
+
onceDiagnostics(uri) {
|
|
139
|
+
return new Promise((resolve) => {
|
|
140
|
+
const list = this.diagWaiters.get(uri) ?? [];
|
|
141
|
+
list.push(resolve);
|
|
142
|
+
this.diagWaiters.set(uri, list);
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
delay(ms) {
|
|
146
|
+
return new Promise((r) => {
|
|
147
|
+
const t = setTimeout(r, ms);
|
|
148
|
+
if (typeof t.unref === 'function')
|
|
149
|
+
t.unref();
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
async waitFor(cond, timeoutMs) {
|
|
153
|
+
const start = Date.now();
|
|
154
|
+
while (!cond() && Date.now() - start < timeoutMs) {
|
|
155
|
+
await this.delay(50);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
teardown() {
|
|
159
|
+
this.initialized = false;
|
|
160
|
+
for (const [, p] of this.pendingRequests)
|
|
161
|
+
p.reject(new Error('LSP server exited'));
|
|
162
|
+
this.pendingRequests.clear();
|
|
163
|
+
this.proc = null;
|
|
164
|
+
}
|
|
165
|
+
async stop() {
|
|
166
|
+
if (!this.proc)
|
|
167
|
+
return;
|
|
168
|
+
try {
|
|
169
|
+
await Promise.race([this.request('shutdown', null), this.delay(1000)]);
|
|
170
|
+
this.notify('exit', null);
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
}
|
|
174
|
+
try {
|
|
175
|
+
this.proc.kill();
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
}
|
|
179
|
+
this.teardown();
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
const clients = new Map();
|
|
183
|
+
export function getOrCreateClient(def, rootDir) {
|
|
184
|
+
const key = `${def.id}::${rootDir}`;
|
|
185
|
+
let client = clients.get(key);
|
|
186
|
+
if (!client) {
|
|
187
|
+
client = new LspClient(def, rootDir);
|
|
188
|
+
clients.set(key, client);
|
|
189
|
+
}
|
|
190
|
+
return client;
|
|
191
|
+
}
|
|
192
|
+
export async function shutdownAllLspClients() {
|
|
193
|
+
const all = Array.from(clients.values());
|
|
194
|
+
clients.clear();
|
|
195
|
+
await Promise.all(all.map((c) => c.stop().catch(() => undefined)));
|
|
196
|
+
}
|
|
197
|
+
export function getActiveLspClientCount() {
|
|
198
|
+
return clients.size;
|
|
199
|
+
}
|
|
200
|
+
export { fileURLToPath };
|
|
201
|
+
//# sourceMappingURL=lsp-client.js.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface LspServerDef {
|
|
2
|
+
id: string;
|
|
3
|
+
extensions: string[];
|
|
4
|
+
command: string;
|
|
5
|
+
args: string[];
|
|
6
|
+
}
|
|
7
|
+
export declare function resolveServerBinary(command: string): string | null;
|
|
8
|
+
export declare function getServerDefs(): LspServerDef[];
|
|
9
|
+
export declare function findServerForFile(absPath: string): {
|
|
10
|
+
def: LspServerDef;
|
|
11
|
+
binary: string;
|
|
12
|
+
} | null;
|
|
13
|
+
export declare function clearResolveCache(): void;
|
|
14
|
+
//# sourceMappingURL=server-registry.d.ts.map
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { configManager } from '../config/config-manager.js';
|
|
4
|
+
const DEFAULT_SERVERS = [
|
|
5
|
+
{
|
|
6
|
+
id: 'typescript',
|
|
7
|
+
extensions: ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.mts', '.cts'],
|
|
8
|
+
command: 'typescript-language-server',
|
|
9
|
+
args: ['--stdio'],
|
|
10
|
+
},
|
|
11
|
+
{ id: 'python', extensions: ['.py', '.pyi'], command: 'pyright-langserver', args: ['--stdio'] },
|
|
12
|
+
{ id: 'rust', extensions: ['.rs'], command: 'rust-analyzer', args: [] },
|
|
13
|
+
{ id: 'go', extensions: ['.go'], command: 'gopls', args: [] },
|
|
14
|
+
{ id: 'ruby', extensions: ['.rb'], command: 'solargraph', args: ['stdio'] },
|
|
15
|
+
];
|
|
16
|
+
function execCandidates(command) {
|
|
17
|
+
if (path.isAbsolute(command))
|
|
18
|
+
return [command];
|
|
19
|
+
if (process.platform === 'win32') {
|
|
20
|
+
const exts = (process.env['PATHEXT'] || '.COM;.EXE;.BAT;.CMD').split(';');
|
|
21
|
+
return [command, ...exts.map((e) => command + e.toLowerCase()), ...exts.map((e) => command + e)];
|
|
22
|
+
}
|
|
23
|
+
return [command];
|
|
24
|
+
}
|
|
25
|
+
const resolveCache = new Map();
|
|
26
|
+
export function resolveServerBinary(command) {
|
|
27
|
+
if (resolveCache.has(command))
|
|
28
|
+
return resolveCache.get(command);
|
|
29
|
+
const tryFile = (p) => {
|
|
30
|
+
try {
|
|
31
|
+
const st = fs.statSync(p);
|
|
32
|
+
if (st.isFile())
|
|
33
|
+
return p;
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
}
|
|
37
|
+
return null;
|
|
38
|
+
};
|
|
39
|
+
let found = null;
|
|
40
|
+
if (path.isAbsolute(command) || command.includes('/') || command.includes('\\')) {
|
|
41
|
+
for (const cand of execCandidates(command)) {
|
|
42
|
+
found = tryFile(path.resolve(cand));
|
|
43
|
+
if (found)
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
const pathDirs = (process.env['PATH'] || '').split(path.delimiter).filter(Boolean);
|
|
49
|
+
outer: for (const dir of pathDirs) {
|
|
50
|
+
for (const cand of execCandidates(command)) {
|
|
51
|
+
found = tryFile(path.join(dir, cand));
|
|
52
|
+
if (found)
|
|
53
|
+
break outer;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
resolveCache.set(command, found);
|
|
58
|
+
return found;
|
|
59
|
+
}
|
|
60
|
+
export function getServerDefs() {
|
|
61
|
+
let custom = [];
|
|
62
|
+
try {
|
|
63
|
+
custom = configManager.getLspServers();
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
}
|
|
67
|
+
return [...custom, ...DEFAULT_SERVERS];
|
|
68
|
+
}
|
|
69
|
+
export function findServerForFile(absPath) {
|
|
70
|
+
const ext = path.extname(absPath).toLowerCase();
|
|
71
|
+
if (!ext)
|
|
72
|
+
return null;
|
|
73
|
+
for (const def of getServerDefs()) {
|
|
74
|
+
if (!def.extensions.includes(ext))
|
|
75
|
+
continue;
|
|
76
|
+
const binary = resolveServerBinary(def.command);
|
|
77
|
+
if (binary)
|
|
78
|
+
return { def: { ...def, command: binary }, binary };
|
|
79
|
+
}
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
export function clearResolveCache() {
|
|
83
|
+
resolveCache.clear();
|
|
84
|
+
}
|
|
85
|
+
//# sourceMappingURL=server-registry.js.map
|
package/dist/eval/eval-runner.js
CHANGED
|
@@ -40,6 +40,12 @@ export class EvalRunner {
|
|
|
40
40
|
if (input.working_dir) {
|
|
41
41
|
process.chdir(input.working_dir);
|
|
42
42
|
}
|
|
43
|
+
try {
|
|
44
|
+
const { initializeMcpServers } = await import('../tools/mcp/index.js');
|
|
45
|
+
await initializeMcpServers(process.cwd());
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
}
|
|
43
49
|
const startEvent = {
|
|
44
50
|
event: 'start',
|
|
45
51
|
timestamp: now(),
|
|
@@ -58,6 +64,14 @@ export class EvalRunner {
|
|
|
58
64
|
this.emitError(error instanceof Error ? error.message : String(error));
|
|
59
65
|
this.emitEnd(false);
|
|
60
66
|
}
|
|
67
|
+
finally {
|
|
68
|
+
try {
|
|
69
|
+
const { shutdownAllLspClients } = await import('../core/lsp/index.js');
|
|
70
|
+
await shutdownAllLspClients();
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
}
|
|
74
|
+
}
|
|
61
75
|
}
|
|
62
76
|
async execute(prompt) {
|
|
63
77
|
if (!this.llmClient) {
|
|
@@ -2,6 +2,7 @@ import { PlanningLLM } from '../agents/planner/index.js';
|
|
|
2
2
|
import { sessionManager } from '../core/session/session-manager.js';
|
|
3
3
|
import { CompactManager, contextTracker, buildCompactedMessages, } from '../core/compact/index.js';
|
|
4
4
|
import { configManager } from '../core/config/config-manager.js';
|
|
5
|
+
import { fileSnapshotStore } from '../core/file-snapshot-store.js';
|
|
5
6
|
import { setTodoWriteCallback, clearTodoCallbacks, } from '../tools/llm/simple/todo-tools.js';
|
|
6
7
|
import { setGetTodosCallback, setFinalResponseCallback, setMarkTodosCompletedCallback, clearFinalResponseCallbacks, } from '../tools/llm/simple/final-response-tool.js';
|
|
7
8
|
import { eventBus, Events } from '../core/event-bus.js';
|
|
@@ -54,6 +55,7 @@ export class PlanExecutor {
|
|
|
54
55
|
});
|
|
55
56
|
this.currentLLMClient = llmClient;
|
|
56
57
|
setDocsSearchLLMClientGetter(() => this.currentLLMClient);
|
|
58
|
+
fileSnapshotStore.beginTurn(userMessage);
|
|
57
59
|
isInterruptedRef.current = false;
|
|
58
60
|
callbacks.setIsInterrupted(false);
|
|
59
61
|
callbacks.setTodos([]);
|
|
@@ -3,6 +3,8 @@ import * as path from 'path';
|
|
|
3
3
|
import { logger } from '../../../utils/logger.js';
|
|
4
4
|
import { shouldIgnore } from '../../../core/ignore-filter.js';
|
|
5
5
|
import { getCachedFile, setCachedFile, invalidateCache } from '../../../core/file-cache.js';
|
|
6
|
+
import { fileSnapshotStore } from '../../../core/file-snapshot-store.js';
|
|
7
|
+
import { getDiagnosticsForFile } from '../../../core/lsp/index.js';
|
|
6
8
|
const EXCLUDED_DIRS = new Set([
|
|
7
9
|
'node_modules',
|
|
8
10
|
'.git',
|
|
@@ -193,10 +195,12 @@ async function _executeCreateFile(args) {
|
|
|
193
195
|
}
|
|
194
196
|
const dir = path.dirname(resolvedPath);
|
|
195
197
|
await fs.mkdir(dir, { recursive: true });
|
|
198
|
+
fileSnapshotStore.record(resolvedPath, null);
|
|
196
199
|
await fs.writeFile(resolvedPath, content, 'utf-8');
|
|
197
200
|
invalidateCache(resolvedPath);
|
|
198
201
|
const lines = content.split('\n').length;
|
|
199
202
|
logger.toolSuccess('create_file', args, { file: displayPath, lines }, 0);
|
|
203
|
+
const diagnostics = await getDiagnosticsForFile(resolvedPath);
|
|
200
204
|
return {
|
|
201
205
|
success: true,
|
|
202
206
|
result: JSON.stringify({
|
|
@@ -204,7 +208,7 @@ async function _executeCreateFile(args) {
|
|
|
204
208
|
file: displayPath,
|
|
205
209
|
lines: lines,
|
|
206
210
|
message: `Created ${displayPath} (${lines} lines)`,
|
|
207
|
-
}),
|
|
211
|
+
}) + diagnostics,
|
|
208
212
|
};
|
|
209
213
|
}
|
|
210
214
|
catch (error) {
|
|
@@ -356,6 +360,7 @@ async function _executeEditFile(args) {
|
|
|
356
360
|
error: `Modified content too large (${(newContentSize / 1024 / 1024).toFixed(2)}MB). Maximum: ${MAX_WRITE_SIZE / 1024 / 1024}MB`,
|
|
357
361
|
};
|
|
358
362
|
}
|
|
363
|
+
fileSnapshotStore.record(resolvedPath, originalContent);
|
|
359
364
|
await fs.writeFile(resolvedPath, newContent, 'utf-8');
|
|
360
365
|
invalidateCache(resolvedPath);
|
|
361
366
|
const oldLinesArr = oldString.split('\n');
|
|
@@ -371,6 +376,7 @@ async function _executeEditFile(args) {
|
|
|
371
376
|
if (newLinesArr.length > 5)
|
|
372
377
|
diffPreview.push('+ ...');
|
|
373
378
|
logger.toolSuccess('edit_file', args, { file: displayPath, replacements, oldLines: oldLinesArr.length, newLines: newLinesArr.length }, 0);
|
|
379
|
+
const diagnostics = await getDiagnosticsForFile(resolvedPath);
|
|
374
380
|
return {
|
|
375
381
|
success: true,
|
|
376
382
|
result: JSON.stringify({
|
|
@@ -383,7 +389,7 @@ async function _executeEditFile(args) {
|
|
|
383
389
|
? `Replaced ${replacements} occurrence(s) in ${displayPath}`
|
|
384
390
|
: `Updated ${displayPath}`,
|
|
385
391
|
diff: diffPreview,
|
|
386
|
-
}),
|
|
392
|
+
}) + diagnostics,
|
|
387
393
|
};
|
|
388
394
|
}
|
|
389
395
|
catch (error) {
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export declare function namespacedToolName(server: string, tool: string): string;
|
|
2
|
+
export interface McpInitResult {
|
|
3
|
+
servers: number;
|
|
4
|
+
tools: number;
|
|
5
|
+
errors: Array<{
|
|
6
|
+
server: string;
|
|
7
|
+
error: string;
|
|
8
|
+
}>;
|
|
9
|
+
}
|
|
10
|
+
export declare function initializeMcpServers(cwd?: string): Promise<McpInitResult>;
|
|
11
|
+
export declare function getConnectedMcpServers(): Array<{
|
|
12
|
+
name: string;
|
|
13
|
+
tools: string[];
|
|
14
|
+
}>;
|
|
15
|
+
export declare function disconnectMcpServers(): Promise<void>;
|
|
16
|
+
//# sourceMappingURL=mcp-client.d.ts.map
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
2
|
+
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
3
|
+
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
|
|
4
|
+
import { createRequire } from 'module';
|
|
5
|
+
import { toolRegistry } from '../registry.js';
|
|
6
|
+
import { loadMcpServerConfigs } from './mcp-config.js';
|
|
7
|
+
import { logger } from '../../utils/logger.js';
|
|
8
|
+
const pkg = createRequire(import.meta.url)('../../../package.json');
|
|
9
|
+
const CONNECT_TIMEOUT_MS = Number(process.env['ORQUESTA_MCP_TIMEOUT_MS']) || 15000;
|
|
10
|
+
const CALL_TIMEOUT_MS = Number(process.env['ORQUESTA_MCP_CALL_TIMEOUT_MS']) || 120000;
|
|
11
|
+
const connected = [];
|
|
12
|
+
export function namespacedToolName(server, tool) {
|
|
13
|
+
const clean = (s) => s.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
14
|
+
return `mcp__${clean(server)}__${clean(tool)}`;
|
|
15
|
+
}
|
|
16
|
+
function flattenToolContent(content) {
|
|
17
|
+
if (!Array.isArray(content)) {
|
|
18
|
+
return typeof content === 'string' ? content : JSON.stringify(content ?? '');
|
|
19
|
+
}
|
|
20
|
+
const parts = [];
|
|
21
|
+
for (const item of content) {
|
|
22
|
+
if (!item || typeof item !== 'object')
|
|
23
|
+
continue;
|
|
24
|
+
const block = item;
|
|
25
|
+
if (block.type === 'text' && typeof block.text === 'string') {
|
|
26
|
+
parts.push(block.text);
|
|
27
|
+
}
|
|
28
|
+
else if (block.type === 'image') {
|
|
29
|
+
parts.push(`[image${block.mimeType ? ` ${block.mimeType}` : ''} — ${block.data ? `${block.data.length} b64 bytes` : 'no data'}]`);
|
|
30
|
+
}
|
|
31
|
+
else if (block.type === 'resource' && block.resource) {
|
|
32
|
+
const r = block.resource;
|
|
33
|
+
parts.push(r.text ?? `[resource ${r.uri ?? ''}]`);
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
parts.push(JSON.stringify(block));
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return parts.join('\n').trim();
|
|
40
|
+
}
|
|
41
|
+
function toToolParameters(inputSchema) {
|
|
42
|
+
const schema = (inputSchema && typeof inputSchema === 'object' ? inputSchema : {});
|
|
43
|
+
return {
|
|
44
|
+
type: 'object',
|
|
45
|
+
properties: schema.properties && typeof schema.properties === 'object' ? schema.properties : {},
|
|
46
|
+
...(Array.isArray(schema.required) ? { required: schema.required } : {}),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function wrapMcpTool(client, serverName, tool) {
|
|
50
|
+
const fqName = namespacedToolName(serverName, tool.name);
|
|
51
|
+
const description = `[MCP:${serverName}] ${tool.description || tool.name}`.slice(0, 1024);
|
|
52
|
+
const definition = {
|
|
53
|
+
type: 'function',
|
|
54
|
+
function: {
|
|
55
|
+
name: fqName,
|
|
56
|
+
description,
|
|
57
|
+
parameters: toToolParameters(tool.inputSchema),
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
return {
|
|
61
|
+
definition,
|
|
62
|
+
categories: ['llm-simple', 'mcp'],
|
|
63
|
+
async execute(args) {
|
|
64
|
+
try {
|
|
65
|
+
const res = await client.callTool({ name: tool.name, arguments: args ?? {} }, undefined, { timeout: CALL_TIMEOUT_MS });
|
|
66
|
+
const text = flattenToolContent(res.content);
|
|
67
|
+
if (res.isError) {
|
|
68
|
+
return { success: false, error: text || `MCP tool ${fqName} reported an error` };
|
|
69
|
+
}
|
|
70
|
+
return { success: true, result: text || '(no output)' };
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
return {
|
|
74
|
+
success: false,
|
|
75
|
+
error: `MCP tool ${fqName} failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
function buildTransport(cfg) {
|
|
82
|
+
if (cfg.url) {
|
|
83
|
+
const options = cfg.headers
|
|
84
|
+
? { requestInit: { headers: cfg.headers } }
|
|
85
|
+
: undefined;
|
|
86
|
+
return new StreamableHTTPClientTransport(new URL(cfg.url), options);
|
|
87
|
+
}
|
|
88
|
+
if (cfg.command) {
|
|
89
|
+
return new StdioClientTransport({
|
|
90
|
+
command: cfg.command,
|
|
91
|
+
args: cfg.args ?? [],
|
|
92
|
+
env: { ...getInheritedEnv(), ...(cfg.env ?? {}) },
|
|
93
|
+
...(cfg.cwd ? { cwd: cfg.cwd } : {}),
|
|
94
|
+
stderr: 'ignore',
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
throw new Error(`MCP server "${cfg.name}" has no command or url`);
|
|
98
|
+
}
|
|
99
|
+
function getInheritedEnv() {
|
|
100
|
+
const out = {};
|
|
101
|
+
for (const [k, v] of Object.entries(process.env)) {
|
|
102
|
+
if (typeof v === 'string')
|
|
103
|
+
out[k] = v;
|
|
104
|
+
}
|
|
105
|
+
return out;
|
|
106
|
+
}
|
|
107
|
+
function withTimeout(promise, ms, label) {
|
|
108
|
+
return new Promise((resolve, reject) => {
|
|
109
|
+
const t = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms);
|
|
110
|
+
if (typeof t.unref === 'function')
|
|
111
|
+
t.unref();
|
|
112
|
+
promise.then((v) => {
|
|
113
|
+
clearTimeout(t);
|
|
114
|
+
resolve(v);
|
|
115
|
+
}, (e) => {
|
|
116
|
+
clearTimeout(t);
|
|
117
|
+
reject(e);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
async function connectOne(cfg) {
|
|
122
|
+
const client = new Client({ name: 'orquesta-cli', version: pkg.version || '0.0.0' }, { capabilities: {} });
|
|
123
|
+
let transport;
|
|
124
|
+
try {
|
|
125
|
+
transport = buildTransport(cfg);
|
|
126
|
+
}
|
|
127
|
+
catch (err) {
|
|
128
|
+
logger.warn('MCP transport build failed', { server: cfg.name, error: err instanceof Error ? err.message : String(err) });
|
|
129
|
+
return 0;
|
|
130
|
+
}
|
|
131
|
+
await withTimeout(client.connect(transport), CONNECT_TIMEOUT_MS, `MCP connect (${cfg.name})`);
|
|
132
|
+
const listed = await withTimeout(client.listTools(), CONNECT_TIMEOUT_MS, `MCP listTools (${cfg.name})`);
|
|
133
|
+
const tools = Array.isArray(listed.tools) ? listed.tools : [];
|
|
134
|
+
const toolNames = [];
|
|
135
|
+
for (const t of tools) {
|
|
136
|
+
if (!t?.name)
|
|
137
|
+
continue;
|
|
138
|
+
const wrapped = wrapMcpTool(client, cfg.name, t);
|
|
139
|
+
toolRegistry.register(wrapped);
|
|
140
|
+
toolNames.push(wrapped.definition.function.name);
|
|
141
|
+
}
|
|
142
|
+
connected.push({ name: cfg.name, client, toolNames });
|
|
143
|
+
logger.info('MCP server connected', { server: cfg.name, tools: toolNames.length });
|
|
144
|
+
return toolNames.length;
|
|
145
|
+
}
|
|
146
|
+
export async function initializeMcpServers(cwd = process.cwd()) {
|
|
147
|
+
const configs = loadMcpServerConfigs(cwd);
|
|
148
|
+
const result = { servers: 0, tools: 0, errors: [] };
|
|
149
|
+
if (configs.length === 0)
|
|
150
|
+
return result;
|
|
151
|
+
logger.enter('initializeMcpServers', { count: configs.length });
|
|
152
|
+
await Promise.all(configs.map(async (cfg) => {
|
|
153
|
+
try {
|
|
154
|
+
const n = await connectOne(cfg);
|
|
155
|
+
result.servers += 1;
|
|
156
|
+
result.tools += n;
|
|
157
|
+
}
|
|
158
|
+
catch (err) {
|
|
159
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
160
|
+
logger.warn('MCP server failed to connect', { server: cfg.name, error });
|
|
161
|
+
result.errors.push({ server: cfg.name, error });
|
|
162
|
+
}
|
|
163
|
+
}));
|
|
164
|
+
logger.exit('initializeMcpServers', result);
|
|
165
|
+
return result;
|
|
166
|
+
}
|
|
167
|
+
export function getConnectedMcpServers() {
|
|
168
|
+
return connected.map((c) => ({ name: c.name, tools: [...c.toolNames] }));
|
|
169
|
+
}
|
|
170
|
+
export async function disconnectMcpServers() {
|
|
171
|
+
await Promise.all(connected.map(async (c) => {
|
|
172
|
+
try {
|
|
173
|
+
await c.client.close();
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
}
|
|
177
|
+
}));
|
|
178
|
+
connected.length = 0;
|
|
179
|
+
}
|
|
180
|
+
//# sourceMappingURL=mcp-client.js.map
|