orbitx 0.0.1 → 0.0.3
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/.env.example +1 -0
- package/Readme.md +81 -0
- package/orbitx.png +0 -0
- package/package.json +17 -2
- package/public/index.html +397 -0
- package/src/core/ai-provider.ts +25 -0
- package/src/core/base-agent.ts +297 -0
- package/src/core/deepseek-provider.ts +71 -0
- package/src/core/mcp-client.ts +113 -0
- package/src/core/mcp-computer.ts +5 -0
- package/src/core/mcp-connection.ts +12 -0
- package/src/core/mcp-ipc-connection.ts +153 -0
- package/src/core/mcp-server.ts +49 -0
- package/src/core/mcp.ts +23 -0
- package/src/core/ollama-provider.ts +70 -0
- package/src/core/skill.ts +16 -0
- package/src/index.ts +17 -0
- package/src/templates/researcher.ts +73 -0
- package/src/templates/simple.ts +64 -0
- package/src/tests/mcp.test.ts +73 -0
- package/src/tools/fs/index.ts +2 -0
- package/src/tools/fs/read-file.ts +54 -0
- package/src/tools/fs/write-file.ts +73 -0
- package/src/tools/utils/get-current-time.ts +45 -0
- package/src/tools/utils/index.ts +1 -0
- package/src/tools/www/fetch.ts +161 -0
- package/src/tools/www/index.ts +3 -0
- package/src/tools/www/read-web-page.ts +350 -0
- package/src/tools/www/web-search.ts +102 -0
- package/tsconfig.json +34 -0
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
import type { AIProvider, Message, StreamCallback } from "./ai-provider";
|
|
2
|
+
import type MCPClient from "./mcp-client";
|
|
3
|
+
import type MCPTool from "./mcp";
|
|
4
|
+
import type Skill from "./skill";
|
|
5
|
+
|
|
6
|
+
export interface ParsedToolCall {
|
|
7
|
+
id: string;
|
|
8
|
+
tool: string;
|
|
9
|
+
inputs: Record<string, any>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type ExtractedSegment =
|
|
13
|
+
| { type: "text"; context: string }
|
|
14
|
+
| { type: "tool"; context: ParsedToolCall };
|
|
15
|
+
|
|
16
|
+
export class BaseAgent {
|
|
17
|
+
#maxMemorizeToken = 16000;
|
|
18
|
+
#instruction: string = "";
|
|
19
|
+
#allowedTools: string[] = [];
|
|
20
|
+
|
|
21
|
+
#skills: Skill[] = [];
|
|
22
|
+
|
|
23
|
+
#memory: string = "";
|
|
24
|
+
|
|
25
|
+
#messagesFull: Message[] = [];
|
|
26
|
+
|
|
27
|
+
#fullInputMissTokens = 0;
|
|
28
|
+
#fullInputHitTokens = 0;
|
|
29
|
+
#fullOutputTokens = 0;
|
|
30
|
+
|
|
31
|
+
#messagesCompact: Message[] = [];
|
|
32
|
+
#currentInputMissTokens = 0;
|
|
33
|
+
#currentInputHitTokens = 0;
|
|
34
|
+
#currentLastOutputTokens = 0;
|
|
35
|
+
#currentOutputTokens = 0;
|
|
36
|
+
|
|
37
|
+
#mcpClient: MCPClient;
|
|
38
|
+
#aiProvider: AIProvider;
|
|
39
|
+
|
|
40
|
+
constructor({ instruction, allowedTools, aiProvider, mcpClient, initData = {
|
|
41
|
+
memory: "",
|
|
42
|
+
messagesFull: [],
|
|
43
|
+
fullInputMissTokens: 0,
|
|
44
|
+
fullInputHitTokens: 0,
|
|
45
|
+
fullOutputTokens: 0,
|
|
46
|
+
messagesCompact: [],
|
|
47
|
+
currentInputMissTokens: 0,
|
|
48
|
+
currentInputHitTokens: 0,
|
|
49
|
+
currentOutputTokens: 0,
|
|
50
|
+
}, skills = [], maxMemorizeToken = 16000 }: {
|
|
51
|
+
instruction: string;
|
|
52
|
+
allowedTools: MCPTool[];
|
|
53
|
+
aiProvider: AIProvider;
|
|
54
|
+
mcpClient: MCPClient;
|
|
55
|
+
skills?: Skill[];
|
|
56
|
+
maxMemorizeToken?: number;
|
|
57
|
+
initData?: {
|
|
58
|
+
memory: string;
|
|
59
|
+
messagesFull: Message[];
|
|
60
|
+
fullInputMissTokens: number;
|
|
61
|
+
fullInputHitTokens: number;
|
|
62
|
+
fullOutputTokens: number;
|
|
63
|
+
messagesCompact: Message[];
|
|
64
|
+
currentInputMissTokens: number;
|
|
65
|
+
currentInputHitTokens: number;
|
|
66
|
+
currentOutputTokens: number;
|
|
67
|
+
}
|
|
68
|
+
}) {
|
|
69
|
+
this.#instruction = instruction;
|
|
70
|
+
this.#allowedTools = allowedTools.map(t => t.getOptions().name);
|
|
71
|
+
this.#aiProvider = aiProvider;
|
|
72
|
+
this.#mcpClient = mcpClient;
|
|
73
|
+
this.#memory = initData.memory;
|
|
74
|
+
this.#messagesFull = initData.messagesFull;
|
|
75
|
+
this.#fullInputMissTokens = initData.fullInputMissTokens
|
|
76
|
+
this.#fullInputHitTokens = initData.fullInputHitTokens
|
|
77
|
+
this.#fullOutputTokens = initData.fullOutputTokens
|
|
78
|
+
this.#messagesCompact = initData.messagesCompact;
|
|
79
|
+
this.#currentInputMissTokens = initData.currentInputMissTokens
|
|
80
|
+
this.#currentInputHitTokens = initData.currentInputHitTokens
|
|
81
|
+
this.#currentOutputTokens = initData.currentOutputTokens
|
|
82
|
+
this.#maxMemorizeToken = maxMemorizeToken;
|
|
83
|
+
this.#skills = skills;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
#extractJson(str: string): ExtractedSegment[] {
|
|
87
|
+
const segments: ExtractedSegment[] = [];
|
|
88
|
+
let cursor = 0;
|
|
89
|
+
let textStart = 0;
|
|
90
|
+
|
|
91
|
+
for (let i = 0; i < str.length; i++) {
|
|
92
|
+
if (str[i] !== "{") continue;
|
|
93
|
+
|
|
94
|
+
let brace = 0;
|
|
95
|
+
for (let j = i; j < str.length; j++) {
|
|
96
|
+
if (str[j] === "{") brace++;
|
|
97
|
+
if (str[j] === "}") brace--;
|
|
98
|
+
|
|
99
|
+
if (brace === 0) {
|
|
100
|
+
const candidate = str.slice(i, j + 1);
|
|
101
|
+
try {
|
|
102
|
+
const result = JSON.parse(candidate);
|
|
103
|
+
if (
|
|
104
|
+
result &&
|
|
105
|
+
typeof result === "object" &&
|
|
106
|
+
"tool" in result &&
|
|
107
|
+
"inputs" in result &&
|
|
108
|
+
typeof result.tool === "string"
|
|
109
|
+
) {
|
|
110
|
+
const text = str.slice(textStart, i);
|
|
111
|
+
if (text.trim().length > 0) {
|
|
112
|
+
segments.push({ type: "text", context: text });
|
|
113
|
+
}
|
|
114
|
+
segments.push({ type: "tool", context: result as ParsedToolCall });
|
|
115
|
+
|
|
116
|
+
i = j;
|
|
117
|
+
textStart = j + 1;
|
|
118
|
+
cursor = j + 1;
|
|
119
|
+
}
|
|
120
|
+
} catch {
|
|
121
|
+
// not valid JSON, skip
|
|
122
|
+
}
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const remaining = str.slice(textStart);
|
|
129
|
+
if (remaining.trim().length > 0) {
|
|
130
|
+
segments.push({ type: "text", context: remaining });
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return segments;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async #buildSystemPrompt(): Promise<string> {
|
|
137
|
+
const tools = (await this.#mcpClient.getTools())
|
|
138
|
+
.filter(t => this.#allowedTools.includes(t.name));
|
|
139
|
+
|
|
140
|
+
const toolBlock = tools.map(t => {
|
|
141
|
+
const params = t.inputs.map(ti =>
|
|
142
|
+
`${ti.name}${ti.required ? "" : "?"}:${ti.type}${ti.default !== undefined ? `=${JSON.stringify(ti.default)}` : ""} // ${ti.description}`
|
|
143
|
+
).join(", ");
|
|
144
|
+
|
|
145
|
+
const example: Record<string, unknown> = {};
|
|
146
|
+
t.inputs.forEach(ti => {
|
|
147
|
+
example[ti.name] = ti.default !== undefined ? ti.default
|
|
148
|
+
: ti.type === "string" ? "..."
|
|
149
|
+
: ti.type === "number" ? 0
|
|
150
|
+
: ti.type === "boolean" ? true
|
|
151
|
+
: ti.type === "array" ? []
|
|
152
|
+
: {};
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
return `${t.name}(${params || ""})\n${t.description}\nex: {"tool":"${t.name}","inputs":${JSON.stringify(example)}}`;
|
|
156
|
+
}).join("\n\n");
|
|
157
|
+
|
|
158
|
+
return `You are an assistant with tools. CRITICAL RULES:
|
|
159
|
+
1. Think step by step.
|
|
160
|
+
${tools.length > 0 && `2. EVERY tool call MUST include a UNIQUE 10-digit random ID in the format: {"id":"1234567890", "tool":"<name>","inputs":{...}}
|
|
161
|
+
3. The ID MUST be exactly 10 digits (numbers only) and MUST be different for EACH tool call.
|
|
162
|
+
4. Example of VALID tool call: {"id":"7845123698", "tool":"search", "inputs":{"query":"weather"}}
|
|
163
|
+
5. After each tool result, continue reasoning or call another tool.
|
|
164
|
+
6. Never invent tool results.`}
|
|
165
|
+
${tools.length > 0 ? "7" : "2"}. If any rules conflict with upper ones, align with upper rules.
|
|
166
|
+
|
|
167
|
+
${tools.length > 0 && `REMEMBER: Every tool call MUST include a random 10-digit ID!
|
|
168
|
+
|
|
169
|
+
TOOLS:
|
|
170
|
+
${toolBlock}
|
|
171
|
+
`}
|
|
172
|
+
|
|
173
|
+
${this.#skills &&`
|
|
174
|
+
SKILLS:
|
|
175
|
+
${this.#skills.map(skill => (`${skill.getSkill().name} - ${skill.getSkill().description}
|
|
176
|
+
${skill.getSkill().instructions}
|
|
177
|
+
\n`))}
|
|
178
|
+
`}
|
|
179
|
+
|
|
180
|
+
MEMORY:
|
|
181
|
+
${this.#memory || "(empty)"}
|
|
182
|
+
|
|
183
|
+
INSTRUCTIONS:
|
|
184
|
+
${this.#instruction}
|
|
185
|
+
`;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
#buildMemoryPrompt(): string {
|
|
189
|
+
return `Summarize all important facts and pending tasks from this conversation.
|
|
190
|
+
Output only this JSON line, nothing else:
|
|
191
|
+
{"tool":"set_memory","inputs":{"new_memory":"<compact summary>"}}`;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async #extractToolCalls(content: string): Promise<ParsedToolCall[]> {
|
|
195
|
+
return await this.#extractJson(content)
|
|
196
|
+
.filter((r: { type: string }) => r.type === "tool")
|
|
197
|
+
.map((t) => t.context as ParsedToolCall);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async #dispatchTool(toolCall: ParsedToolCall): Promise<string> {
|
|
201
|
+
if (toolCall.tool === "set_memory") {
|
|
202
|
+
if (typeof toolCall.inputs?.new_memory === "string") {
|
|
203
|
+
this.#memory = toolCall.inputs.new_memory;
|
|
204
|
+
return "Memory updated.";
|
|
205
|
+
}
|
|
206
|
+
return `Error: set_memory requires new_memory:string, got ${JSON.stringify(toolCall.inputs)}`;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (this.#allowedTools.includes(toolCall.tool)) {
|
|
210
|
+
return JSON.stringify({ id: toolCall.id, output: await this.#mcpClient.callTool(toolCall.tool, toolCall.inputs) });
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return `Error: tool "${toolCall.tool}" not found or not allowed.`;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async run(prompt: string, streamCallback?: StreamCallback): Promise<void> {
|
|
217
|
+
let toolsCalls: ParsedToolCall[] = [];
|
|
218
|
+
|
|
219
|
+
do {
|
|
220
|
+
const systemPrompt = await this.#buildSystemPrompt();
|
|
221
|
+
|
|
222
|
+
const chat = await this.#aiProvider.chat([
|
|
223
|
+
{ role: "system", content: systemPrompt },
|
|
224
|
+
...this.#messagesCompact,
|
|
225
|
+
{ role: "user", content: prompt }
|
|
226
|
+
], streamCallback);
|
|
227
|
+
|
|
228
|
+
this.#messagesFull.push({ role: "user", content: prompt });
|
|
229
|
+
this.#messagesCompact.push({ role: "user", content: prompt });
|
|
230
|
+
this.#messagesFull.push({ role: "assistant", content: chat.content });
|
|
231
|
+
this.#messagesCompact.push({ role: "assistant", content: chat.content });
|
|
232
|
+
|
|
233
|
+
this.#currentInputHitTokens += this.#currentInputMissTokens;
|
|
234
|
+
this.#currentInputMissTokens += chat.inputTokens - this.#currentInputMissTokens;
|
|
235
|
+
this.#currentOutputTokens += chat.outputTokens;
|
|
236
|
+
this.#currentLastOutputTokens = chat.outputTokens;
|
|
237
|
+
|
|
238
|
+
toolsCalls = await this.#extractToolCalls(chat.content);
|
|
239
|
+
|
|
240
|
+
for (const toolCall of toolsCalls) {
|
|
241
|
+
const toolResponse = await this.#dispatchTool(toolCall);
|
|
242
|
+
this.#messagesFull.push({ role: "system", content: toolResponse });
|
|
243
|
+
this.#messagesCompact.push({ role: "system", content: toolResponse });
|
|
244
|
+
streamCallback?.({ role: "tool", content: toolResponse, done: true });
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (this.#currentInputMissTokens + this.#currentLastOutputTokens > this.#maxMemorizeToken) {
|
|
248
|
+
const memChat = await this.#aiProvider.chat([
|
|
249
|
+
{ role: "system", content: systemPrompt },
|
|
250
|
+
...this.#messagesCompact,
|
|
251
|
+
{ role: "user", content: this.#buildMemoryPrompt() }
|
|
252
|
+
]);
|
|
253
|
+
|
|
254
|
+
this.#currentInputHitTokens += this.#currentInputMissTokens;
|
|
255
|
+
this.#currentInputMissTokens += memChat.inputTokens - this.#currentInputMissTokens;
|
|
256
|
+
this.#currentOutputTokens += memChat.outputTokens;
|
|
257
|
+
|
|
258
|
+
for (const mc of await this.#extractToolCalls(memChat.content)) {
|
|
259
|
+
const toolResponse = await this.#dispatchTool(mc);
|
|
260
|
+
streamCallback?.({ role: "tool", content: toolResponse, done: true });
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
this.#messagesCompact = [];
|
|
264
|
+
this.#fullInputHitTokens += this.#currentInputHitTokens;
|
|
265
|
+
this.#fullInputMissTokens += this.#currentInputMissTokens;
|
|
266
|
+
this.#fullOutputTokens += this.#currentOutputTokens;
|
|
267
|
+
this.#currentInputHitTokens = 0;
|
|
268
|
+
this.#currentInputMissTokens = 0;
|
|
269
|
+
this.#currentOutputTokens = 0;
|
|
270
|
+
this.#currentLastOutputTokens = 0;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
prompt = "";
|
|
274
|
+
|
|
275
|
+
} while (toolsCalls.length > 0);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
getCurrentTotalTokens() {
|
|
279
|
+
return {
|
|
280
|
+
total: this.#currentInputHitTokens + this.#currentInputMissTokens + this.#currentOutputTokens,
|
|
281
|
+
inputHit: this.#currentInputHitTokens,
|
|
282
|
+
inputMiss: this.#currentInputMissTokens,
|
|
283
|
+
output: this.#currentOutputTokens
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
getFullTotalTokens() {
|
|
288
|
+
return {
|
|
289
|
+
total: this.#fullInputHitTokens + this.#fullInputMissTokens + this.#fullOutputTokens,
|
|
290
|
+
inputHit: this.#fullInputHitTokens,
|
|
291
|
+
inputMiss: this.#fullInputMissTokens,
|
|
292
|
+
output: this.#fullOutputTokens
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export default BaseAgent;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import OpenAI from "openai";
|
|
2
|
+
import AIProvider, { type Message, type ChatResponse, type StreamCallback } from "./ai-provider";
|
|
3
|
+
|
|
4
|
+
export class DeepSeekProvider extends AIProvider {
|
|
5
|
+
private client: OpenAI;
|
|
6
|
+
private model: string;
|
|
7
|
+
|
|
8
|
+
constructor(apiKey: string, model: string = "deepseek-v4-flash") {
|
|
9
|
+
super();
|
|
10
|
+
this.client = new OpenAI({
|
|
11
|
+
apiKey: apiKey,
|
|
12
|
+
baseURL: "https://api.deepseek.com"
|
|
13
|
+
});
|
|
14
|
+
this.model = model;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async chat(
|
|
18
|
+
messages: Message[],
|
|
19
|
+
streamCallback?: StreamCallback
|
|
20
|
+
): Promise<ChatResponse> {
|
|
21
|
+
const formattedMessages = messages.map(msg => ({
|
|
22
|
+
role: msg.role,
|
|
23
|
+
content: msg.content || ""
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
if (streamCallback) {
|
|
27
|
+
const stream = await this.client.chat.completions.create({
|
|
28
|
+
model: this.model,
|
|
29
|
+
messages: formattedMessages as any,
|
|
30
|
+
stream: true
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
let fullContent = "";
|
|
34
|
+
let inputTokens = 0;
|
|
35
|
+
let outputTokens = 0;
|
|
36
|
+
|
|
37
|
+
for await (const chunk of stream) {
|
|
38
|
+
const content = chunk.choices[0]?.delta?.content || "";
|
|
39
|
+
if (content) {
|
|
40
|
+
fullContent += content;
|
|
41
|
+
streamCallback({ role: "assistant", content, done: false });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (chunk.usage) {
|
|
45
|
+
inputTokens = chunk.usage.prompt_tokens || 0;
|
|
46
|
+
outputTokens = chunk.usage.completion_tokens || 0;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
streamCallback({ role: "assistant", content: "", done: true });
|
|
51
|
+
return {
|
|
52
|
+
content: fullContent,
|
|
53
|
+
inputTokens,
|
|
54
|
+
outputTokens,
|
|
55
|
+
};
|
|
56
|
+
} else {
|
|
57
|
+
const response = await this.client.chat.completions.create({
|
|
58
|
+
model: this.model,
|
|
59
|
+
messages: formattedMessages as any
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
content: response.choices[0]?.message?.content || "",
|
|
64
|
+
inputTokens: response.usage?.prompt_tokens || 0,
|
|
65
|
+
outputTokens: response.usage?.completion_tokens || 0,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export default DeepSeekProvider;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import type MCPTool from "./mcp";
|
|
2
|
+
import type MCPConnection from "./mcp-connection";
|
|
3
|
+
import { randomUUID } from "crypto";
|
|
4
|
+
|
|
5
|
+
export class MCPClient {
|
|
6
|
+
#connections;
|
|
7
|
+
#tools: MCPTool[] = [];
|
|
8
|
+
#envID;
|
|
9
|
+
constructor(envID: string, connection: MCPConnection | MCPConnection[]) {
|
|
10
|
+
this.#connections = Array.isArray(connection) ? connection : [connection];
|
|
11
|
+
this.#envID = envID;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async getTools() {
|
|
15
|
+
const connectionPromises = this.#connections.map(conn => {
|
|
16
|
+
return new Promise<{
|
|
17
|
+
name: string;
|
|
18
|
+
description: string;
|
|
19
|
+
inputs: {
|
|
20
|
+
name: string;
|
|
21
|
+
type: "number" | "string" | "boolean" | "object" | "array";
|
|
22
|
+
description: string;
|
|
23
|
+
required?: boolean;
|
|
24
|
+
default?: any;
|
|
25
|
+
}[];
|
|
26
|
+
}[]>((resolve) => {
|
|
27
|
+
const pid = randomUUID();
|
|
28
|
+
let resolved = false;
|
|
29
|
+
|
|
30
|
+
const onRead = (data: any) => {
|
|
31
|
+
if (data.topic === "getToolsCallback" && data.pid === pid) {
|
|
32
|
+
resolved = true;
|
|
33
|
+
conn.off("read", onRead);
|
|
34
|
+
resolve(data.tools || []);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
conn.on("read", onRead);
|
|
39
|
+
conn.emit("write", {
|
|
40
|
+
pid,
|
|
41
|
+
topic: "getTools"
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
setTimeout(() => {
|
|
45
|
+
if (!resolved) {
|
|
46
|
+
conn.off("read", onRead);
|
|
47
|
+
resolve([]);
|
|
48
|
+
}
|
|
49
|
+
}, 2000);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const results = await Promise.all(connectionPromises);
|
|
54
|
+
|
|
55
|
+
const connectionsTools = results.flat();
|
|
56
|
+
return [...this.#tools.map(t => ({
|
|
57
|
+
name: t.getOptions().name,
|
|
58
|
+
description: t.getOptions().description,
|
|
59
|
+
inputs: t.getOptions().inputs
|
|
60
|
+
})), ...connectionsTools];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async callTool(toolName: string, inputs: Record<string, any>) {
|
|
64
|
+
const clientTool = this.#tools.find(t => t.getOptions().name == toolName);
|
|
65
|
+
if (clientTool) {
|
|
66
|
+
return JSON.stringify(await clientTool.getOptions().execute(this.#envID, inputs));
|
|
67
|
+
} else {
|
|
68
|
+
return JSON.stringify(await new Promise<any>((resolve) => {
|
|
69
|
+
let resolvedResult = false;
|
|
70
|
+
this.#connections.map(conn => {
|
|
71
|
+
const pid = randomUUID();
|
|
72
|
+
let resolved = false;
|
|
73
|
+
|
|
74
|
+
const onRead = (data: any) => {
|
|
75
|
+
if (data.topic === "toolCallCallback" && data.pid === pid) {
|
|
76
|
+
resolved = true;
|
|
77
|
+
conn.off("read", onRead);
|
|
78
|
+
if (!resolvedResult) {
|
|
79
|
+
resolve(data.output || {});
|
|
80
|
+
resolvedResult = true;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
conn.on("read", onRead);
|
|
86
|
+
conn.emit("write", {
|
|
87
|
+
pid,
|
|
88
|
+
topic: "toolCall",
|
|
89
|
+
tool: toolName,
|
|
90
|
+
envID: this.#envID,
|
|
91
|
+
inputs
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
setTimeout(() => {
|
|
95
|
+
if (!resolved) {
|
|
96
|
+
conn.off("read", onRead);
|
|
97
|
+
}
|
|
98
|
+
if (!resolvedResult) {
|
|
99
|
+
resolve({ error: "The tool call exceeded the 300second timeout limit." });
|
|
100
|
+
resolvedResult = true;
|
|
101
|
+
}
|
|
102
|
+
}, 300000);
|
|
103
|
+
});
|
|
104
|
+
}));
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
registerTool(tool: MCPTool) {
|
|
109
|
+
this.#tools.push(tool);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export default MCPClient;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import EventEmitter from "events";
|
|
2
|
+
|
|
3
|
+
export class MCPConnection extends EventEmitter {
|
|
4
|
+
constructor(overrided: boolean = false) {
|
|
5
|
+
super();
|
|
6
|
+
|
|
7
|
+
if (!overrided)
|
|
8
|
+
this.on("write", (data) => this.emit("read", data))
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export default MCPConnection;
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import * as net from "net";
|
|
2
|
+
import * as fs from "fs";
|
|
3
|
+
import MCPConnection from "./mcp-connection";
|
|
4
|
+
|
|
5
|
+
type Role = "server" | "client" | "pending";
|
|
6
|
+
|
|
7
|
+
export class MCPIPCConnection extends MCPConnection {
|
|
8
|
+
private readonly socketPath: string;
|
|
9
|
+
private socket: net.Socket | null = null;
|
|
10
|
+
private server: net.Server | null = null;
|
|
11
|
+
private role: Role = "pending";
|
|
12
|
+
private writeBuffer: Buffer[] = [];
|
|
13
|
+
private readBuffer = "";
|
|
14
|
+
|
|
15
|
+
constructor(socketPath: string) {
|
|
16
|
+
super(true);
|
|
17
|
+
this.socketPath = socketPath;
|
|
18
|
+
|
|
19
|
+
this.on("write", (data: unknown) => {
|
|
20
|
+
this._sendJSON(data);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
setTimeout(() => {
|
|
24
|
+
this._tryConnect();
|
|
25
|
+
}, Math.random() * 200); // Pervent Race Condtion
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
close(): void {
|
|
29
|
+
this.socket?.destroy();
|
|
30
|
+
this.server?.close();
|
|
31
|
+
this._cleanSocketFile();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
private _tryConnect(): void {
|
|
35
|
+
const sock = net.createConnection({ path: this.socketPath });
|
|
36
|
+
|
|
37
|
+
sock.once("connect", () => {
|
|
38
|
+
this.role = "client";
|
|
39
|
+
this.emit("role", "client");
|
|
40
|
+
this._attachSocket(sock);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
sock.once("error", (err: NodeJS.ErrnoException) => {
|
|
44
|
+
if (err.code === "ENOENT" || err.code === "ECONNREFUSED") {
|
|
45
|
+
sock.destroy();
|
|
46
|
+
this._becomeServer();
|
|
47
|
+
} else {
|
|
48
|
+
this.emit("error", err);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
private _becomeServer(): void {
|
|
54
|
+
this._cleanSocketFile();
|
|
55
|
+
|
|
56
|
+
const srv = net.createServer((peer) => {
|
|
57
|
+
this.role = "server";
|
|
58
|
+
this.emit("role", "server");
|
|
59
|
+
|
|
60
|
+
srv.close();
|
|
61
|
+
|
|
62
|
+
this._attachSocket(peer);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
srv.on("error", (err) => this.emit("error", err));
|
|
66
|
+
|
|
67
|
+
srv.listen({ path: this.socketPath }, () => {
|
|
68
|
+
this.server = srv;
|
|
69
|
+
this.emit("listening", this.socketPath);
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
private _attachSocket(sock: net.Socket): void {
|
|
74
|
+
this.socket = sock;
|
|
75
|
+
|
|
76
|
+
for (const chunk of this.writeBuffer) {
|
|
77
|
+
sock.write(chunk);
|
|
78
|
+
}
|
|
79
|
+
this.writeBuffer = [];
|
|
80
|
+
|
|
81
|
+
sock.setEncoding("utf8");
|
|
82
|
+
|
|
83
|
+
sock.on("data", (chunk: string) => {
|
|
84
|
+
this._handleIncoming(chunk);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
sock.on("end", () => {
|
|
88
|
+
this.emit("disconnected");
|
|
89
|
+
this._maybeReconnect();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
sock.on("error", (err: NodeJS.ErrnoException) => {
|
|
93
|
+
this.emit("error", err);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
this.emit("connected", this.role);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private _sendJSON(data: unknown): void {
|
|
100
|
+
let line: string;
|
|
101
|
+
try {
|
|
102
|
+
line = JSON.stringify(data) + "\n";
|
|
103
|
+
} catch (err) {
|
|
104
|
+
this.emit("error", new Error(`MCPIPCConnection: cannot serialise data – ${err}`));
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const chunk = Buffer.from(line, "utf8");
|
|
109
|
+
|
|
110
|
+
if (this.socket && !this.socket.destroyed) {
|
|
111
|
+
this.socket.write(chunk);
|
|
112
|
+
} else {
|
|
113
|
+
this.writeBuffer.push(chunk);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private _handleIncoming(chunk: string): void {
|
|
118
|
+
this.readBuffer += chunk;
|
|
119
|
+
|
|
120
|
+
const lines = this.readBuffer.split("\n");
|
|
121
|
+
|
|
122
|
+
this.readBuffer = lines.pop() ?? "";
|
|
123
|
+
|
|
124
|
+
for (const line of lines) {
|
|
125
|
+
const trimmed = line.trim();
|
|
126
|
+
if (!trimmed) continue;
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
const parsed = JSON.parse(trimmed);
|
|
130
|
+
this.emit("read", parsed);
|
|
131
|
+
} catch {
|
|
132
|
+
this.emit("error", new Error(`MCPIPCConnection: malformed JSON – ${trimmed}`));
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private _cleanSocketFile(): void {
|
|
138
|
+
try {
|
|
139
|
+
fs.unlinkSync(this.socketPath);
|
|
140
|
+
} catch { }
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
private _maybeReconnect(): void {
|
|
144
|
+
if (this.role === "client") {
|
|
145
|
+
setTimeout(() => {
|
|
146
|
+
this.role = "pending";
|
|
147
|
+
this._tryConnect();
|
|
148
|
+
}, 500);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export default MCPIPCConnection;
|