@xalia/agent 0.5.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/.prettierrc.json +11 -0
- package/README.md +56 -0
- package/dist/agent.js +238 -0
- package/dist/agentUtils.js +106 -0
- package/dist/chat.js +296 -0
- package/dist/dummyLLM.js +38 -0
- package/dist/files.js +115 -0
- package/dist/iplatform.js +2 -0
- package/dist/llm.js +2 -0
- package/dist/main.js +147 -0
- package/dist/mcpServerManager.js +278 -0
- package/dist/nodePlatform.js +61 -0
- package/dist/openAILLM.js +38 -0
- package/dist/openAILLMStreaming.js +431 -0
- package/dist/options.js +79 -0
- package/dist/prompt.js +83 -0
- package/dist/sudoMcpServerManager.js +183 -0
- package/dist/test/imageLoad.test.js +14 -0
- package/dist/test/mcpServerManager.test.js +71 -0
- package/dist/test/prompt.test.js +26 -0
- package/dist/test/sudoMcpServerManager.test.js +49 -0
- package/dist/tokenAuth.js +39 -0
- package/dist/tools.js +44 -0
- package/eslint.config.mjs +25 -0
- package/frog.png +0 -0
- package/package.json +42 -0
- package/scripts/git_message +31 -0
- package/scripts/git_wip +21 -0
- package/scripts/pr_message +18 -0
- package/scripts/pr_review +16 -0
- package/scripts/sudomcp_import +23 -0
- package/scripts/test_script +60 -0
- package/src/agent.ts +283 -0
- package/src/agentUtils.ts +198 -0
- package/src/chat.ts +346 -0
- package/src/dummyLLM.ts +50 -0
- package/src/files.ts +95 -0
- package/src/iplatform.ts +17 -0
- package/src/llm.ts +15 -0
- package/src/main.ts +187 -0
- package/src/mcpServerManager.ts +371 -0
- package/src/nodePlatform.ts +24 -0
- package/src/openAILLM.ts +51 -0
- package/src/openAILLMStreaming.ts +528 -0
- package/src/options.ts +103 -0
- package/src/prompt.ts +93 -0
- package/src/sudoMcpServerManager.ts +278 -0
- package/src/test/imageLoad.test.ts +14 -0
- package/src/test/mcpServerManager.test.ts +98 -0
- package/src/test/prompt.test.src +0 -0
- package/src/test/prompt.test.ts +26 -0
- package/src/test/sudoMcpServerManager.test.ts +65 -0
- package/src/tokenAuth.ts +50 -0
- package/src/tools.ts +57 -0
- package/test_data/background_test_profile.json +6 -0
- package/test_data/background_test_script.json +11 -0
- package/test_data/dummyllm_script_simplecalc.json +28 -0
- package/test_data/git_message_profile.json +4 -0
- package/test_data/git_wip_system.txt +5 -0
- package/test_data/pr_message_profile.json +4 -0
- package/test_data/pr_review_profile.json +4 -0
- package/test_data/prompt_simplecalc.txt +1 -0
- package/test_data/simplecalc_profile.json +4 -0
- package/test_data/sudomcp_import_profile.json +4 -0
- package/test_data/test_script_profile.json +8 -0
- package/tsconfig.json +13 -0
@@ -0,0 +1,371 @@
|
|
1
|
+
import { ChatCompletionTool } from "openai/resources.mjs";
|
2
|
+
import {
|
3
|
+
SSEClientTransport,
|
4
|
+
SSEClientTransportOptions,
|
5
|
+
} from "@modelcontextprotocol/sdk/client/sse.js";
|
6
|
+
import { Client as McpClient } from "@modelcontextprotocol/sdk/client/index.js";
|
7
|
+
import { TokenAuth } from "./tokenAuth";
|
8
|
+
import { Tool } from "@modelcontextprotocol/sdk/types.js";
|
9
|
+
import { strict as assert } from "assert";
|
10
|
+
import { McpServerSettings, getLogger } from "@xalia/xmcp/sdk";
|
11
|
+
export type { McpServerSettings } from "@xalia/xmcp/sdk";
|
12
|
+
|
13
|
+
const logger = getLogger();
|
14
|
+
|
15
|
+
/// Callback into an Mcp server
|
16
|
+
export type McpCallback = { (args: string): Promise<string> };
|
17
|
+
|
18
|
+
/// Map of tool name to callback
|
19
|
+
export type McpCallbacks = { [toolName: string]: McpCallback };
|
20
|
+
|
21
|
+
/// List of tool names that are enabled. We keep it as a map in the runtime
|
22
|
+
/// object, so that we can quickly add and remove arbitrary entries. (The
|
23
|
+
/// AgentProfile keeps this as a flat list).
|
24
|
+
export type EnabledToolsMap = { [toolName: string]: boolean };
|
25
|
+
|
26
|
+
/**
|
27
|
+
* The (read-only) McpServerInfo to expose to external classes. Callers
|
28
|
+
* should not modify this data directly. Only through the McpServerManager
|
29
|
+
* class.
|
30
|
+
*/
|
31
|
+
export class McpServerInfo {
|
32
|
+
private readonly tools: Tool[]; // TODO: May not need both tools and toolsMap
|
33
|
+
private readonly toolsMap: { [toolName: string]: Tool };
|
34
|
+
protected enabledToolsMap: EnabledToolsMap;
|
35
|
+
|
36
|
+
constructor(tools: Tool[]) {
|
37
|
+
const toolsMap: { [toolName: string]: Tool } = {};
|
38
|
+
|
39
|
+
for (const mcpTool of tools) {
|
40
|
+
const toolName = mcpTool.name;
|
41
|
+
toolsMap[toolName] = mcpTool;
|
42
|
+
}
|
43
|
+
|
44
|
+
this.tools = tools;
|
45
|
+
this.toolsMap = toolsMap;
|
46
|
+
this.enabledToolsMap = {};
|
47
|
+
}
|
48
|
+
|
49
|
+
public getEnabledTools(): EnabledToolsMap {
|
50
|
+
return this.enabledToolsMap;
|
51
|
+
}
|
52
|
+
|
53
|
+
public getTools(): Tool[] {
|
54
|
+
return this.tools;
|
55
|
+
}
|
56
|
+
|
57
|
+
public getTool(toolName: string): Tool {
|
58
|
+
return this.toolsMap[toolName];
|
59
|
+
}
|
60
|
+
}
|
61
|
+
|
62
|
+
/**
|
63
|
+
* The internal class holds server info and allows it to be updated. Managed
|
64
|
+
* by McpServerManager. Do not access these methods except via the
|
65
|
+
* McpServerManager.
|
66
|
+
*/
|
67
|
+
class McpServerInfoInternal extends McpServerInfo {
|
68
|
+
private readonly client: McpClient;
|
69
|
+
private readonly callbacks: McpCallbacks;
|
70
|
+
|
71
|
+
constructor(client: McpClient, tools: Tool[]) {
|
72
|
+
super(tools);
|
73
|
+
|
74
|
+
const callbacks: McpCallbacks = {};
|
75
|
+
|
76
|
+
for (const mcpTool of tools) {
|
77
|
+
const toolName = mcpTool.name;
|
78
|
+
|
79
|
+
// Create callback
|
80
|
+
const callback = async (argStr: string): Promise<string> => {
|
81
|
+
logger.debug(
|
82
|
+
`cb for ${toolName} invoked with args (${typeof argStr}): ` +
|
83
|
+
`${JSON.stringify(argStr)}`
|
84
|
+
);
|
85
|
+
|
86
|
+
const argsObj = JSON.parse(argStr);
|
87
|
+
const toolResult = await client.callTool({
|
88
|
+
name: toolName,
|
89
|
+
arguments: argsObj,
|
90
|
+
});
|
91
|
+
logger.debug(
|
92
|
+
`cb for ${toolName} returned: ${JSON.stringify(toolResult)}`
|
93
|
+
);
|
94
|
+
|
95
|
+
assert(typeof toolResult === "object");
|
96
|
+
const content = toolResult.content as { [a: number]: unknown };
|
97
|
+
assert(typeof content === "object");
|
98
|
+
assert(content);
|
99
|
+
const content0 = content[0] as { text: string };
|
100
|
+
assert(typeof content0 === "object");
|
101
|
+
const content0Text = content0.text;
|
102
|
+
assert(typeof content0Text === "string");
|
103
|
+
return content0Text;
|
104
|
+
};
|
105
|
+
|
106
|
+
callbacks[toolName] = callback;
|
107
|
+
}
|
108
|
+
|
109
|
+
this.client = client;
|
110
|
+
this.callbacks = callbacks;
|
111
|
+
}
|
112
|
+
|
113
|
+
public async shutdown(): Promise<void> {
|
114
|
+
await this.client.close();
|
115
|
+
}
|
116
|
+
|
117
|
+
public enableTool(toolName: string) {
|
118
|
+
this.enabledToolsMap[toolName] = true;
|
119
|
+
}
|
120
|
+
|
121
|
+
public disableTool(toolName: string) {
|
122
|
+
delete this.enabledToolsMap[toolName];
|
123
|
+
}
|
124
|
+
|
125
|
+
public getCallback(toolName: string) {
|
126
|
+
return this.callbacks[toolName];
|
127
|
+
}
|
128
|
+
}
|
129
|
+
|
130
|
+
/**
|
131
|
+
* Manage a set of MCP servers, where the tools for each server have an
|
132
|
+
* 'enabled' flag. Tools are disabled by default. The set of enabled tools
|
133
|
+
* over all servers is exposed as a single list of OpenAI functions.
|
134
|
+
*/
|
135
|
+
export class McpServerManager {
|
136
|
+
private mcpServers: { [serverName: string]: McpServerInfoInternal } = {};
|
137
|
+
private enabledToolsDirty: boolean = true;
|
138
|
+
private enabledOpenAITools: ChatCompletionTool[] = [];
|
139
|
+
|
140
|
+
public async shutdown() {
|
141
|
+
await Promise.all(
|
142
|
+
Object.keys(this.mcpServers).map((name) => {
|
143
|
+
logger.debug(`shutting down: ${name}...`);
|
144
|
+
this.mcpServers[name].shutdown();
|
145
|
+
})
|
146
|
+
);
|
147
|
+
|
148
|
+
this.mcpServers = {};
|
149
|
+
}
|
150
|
+
|
151
|
+
public getMcpServerNames(): string[] {
|
152
|
+
return Object.keys(this.mcpServers);
|
153
|
+
}
|
154
|
+
|
155
|
+
public getMcpServer(mcpServerName: string): McpServerInfo {
|
156
|
+
return this.getMcpServerInternal(mcpServerName);
|
157
|
+
}
|
158
|
+
|
159
|
+
public async addMcpServer(
|
160
|
+
mcpServerName: string,
|
161
|
+
url: string,
|
162
|
+
apiKey?: string,
|
163
|
+
tools?: Tool[]
|
164
|
+
): Promise<void> {
|
165
|
+
logger.debug(`Adding mcp server ${mcpServerName}: ${url}`);
|
166
|
+
const sseTransportOptions: SSEClientTransportOptions = {};
|
167
|
+
if (apiKey) {
|
168
|
+
sseTransportOptions.authProvider = new TokenAuth(apiKey);
|
169
|
+
}
|
170
|
+
const urlO = new URL(url);
|
171
|
+
const transport = new SSEClientTransport(urlO, sseTransportOptions);
|
172
|
+
const client = new McpClient({
|
173
|
+
name: "@xalia/agent",
|
174
|
+
version: "1.0.0",
|
175
|
+
});
|
176
|
+
|
177
|
+
try {
|
178
|
+
await client.connect(transport);
|
179
|
+
} catch (e) {
|
180
|
+
// TODO: is this catch necessary?
|
181
|
+
await client.close();
|
182
|
+
throw e;
|
183
|
+
}
|
184
|
+
await this.addMcpServerWithClient(client, mcpServerName, tools);
|
185
|
+
}
|
186
|
+
|
187
|
+
/**
|
188
|
+
* Add MCP server from an already connected McpClient.
|
189
|
+
*/
|
190
|
+
public async addMcpServerWithClient(
|
191
|
+
client: McpClient,
|
192
|
+
mcpServerName: string,
|
193
|
+
tools?: Tool[]
|
194
|
+
): Promise<void> {
|
195
|
+
try {
|
196
|
+
// TODO; require the tools to be passed in.
|
197
|
+
|
198
|
+
if (!tools) {
|
199
|
+
const mcpTools = await client.listTools();
|
200
|
+
tools = mcpTools.tools;
|
201
|
+
}
|
202
|
+
|
203
|
+
this.mcpServers[mcpServerName] = new McpServerInfoInternal(client, tools);
|
204
|
+
} catch (e) {
|
205
|
+
await client.close();
|
206
|
+
throw e;
|
207
|
+
}
|
208
|
+
}
|
209
|
+
|
210
|
+
public async removeMcpServer(mcpServerName: string) {
|
211
|
+
const server = this.getMcpServerInternal(mcpServerName);
|
212
|
+
delete this.mcpServers[mcpServerName];
|
213
|
+
await server.shutdown();
|
214
|
+
this.enabledToolsDirty = true;
|
215
|
+
}
|
216
|
+
|
217
|
+
public enableAllTools(mcpServerName: string) {
|
218
|
+
logger.debug(`enableAllTools: ${mcpServerName}`);
|
219
|
+
const server = this.getMcpServerInternal(mcpServerName);
|
220
|
+
for (const tool of server.getTools()) {
|
221
|
+
logger.debug(`enable: ${tool.name}`);
|
222
|
+
server.enableTool(tool.name);
|
223
|
+
}
|
224
|
+
this.enabledToolsDirty = true;
|
225
|
+
}
|
226
|
+
|
227
|
+
public disableAllTools(mcpServerName: string) {
|
228
|
+
logger.debug(`disableAllTools: ${mcpServerName}`);
|
229
|
+
const server = this.getMcpServerInternal(mcpServerName);
|
230
|
+
for (const tool of server.getTools()) {
|
231
|
+
logger.debug(`disable: ${tool.name}`);
|
232
|
+
server.disableTool(tool.name);
|
233
|
+
}
|
234
|
+
this.enabledToolsDirty = true;
|
235
|
+
}
|
236
|
+
|
237
|
+
public enableTool(mcpServerName: string, toolName: string) {
|
238
|
+
logger.debug(`enableTool: ${mcpServerName} ${toolName}`);
|
239
|
+
const server = this.getMcpServerInternal(mcpServerName);
|
240
|
+
server.enableTool(toolName);
|
241
|
+
this.enabledToolsDirty = true;
|
242
|
+
}
|
243
|
+
|
244
|
+
public disableTool(mcpServerName: string, toolName: string) {
|
245
|
+
const server = this.getMcpServerInternal(mcpServerName);
|
246
|
+
server.disableTool(toolName);
|
247
|
+
this.enabledToolsDirty = true;
|
248
|
+
}
|
249
|
+
|
250
|
+
public getOpenAITools(): ChatCompletionTool[] {
|
251
|
+
if (this.enabledToolsDirty) {
|
252
|
+
this.enabledOpenAITools = computeOpenAIToolList(this.mcpServers);
|
253
|
+
this.enabledToolsDirty = false;
|
254
|
+
}
|
255
|
+
|
256
|
+
return this.enabledOpenAITools;
|
257
|
+
}
|
258
|
+
|
259
|
+
/**
|
260
|
+
* Note the `qualifiedToolName` is the full `{mcpServerName}/{toolName}` as
|
261
|
+
* in the openai spec.
|
262
|
+
*/
|
263
|
+
public async invoke(
|
264
|
+
qualifiedToolName: string,
|
265
|
+
args: unknown
|
266
|
+
): Promise<string> {
|
267
|
+
const [mcpServerName, toolName] = splitQualifiedName(qualifiedToolName);
|
268
|
+
logger.debug(`invoke: qualified: ${qualifiedToolName}`);
|
269
|
+
logger.debug(
|
270
|
+
`invoke: mcpServerName: ${mcpServerName}, toolName: ${toolName}`
|
271
|
+
);
|
272
|
+
logger.debug(`invoke: args: ${JSON.stringify(args)}`);
|
273
|
+
|
274
|
+
const server = this.getMcpServerInternal(mcpServerName);
|
275
|
+
const cb = server.getCallback(toolName);
|
276
|
+
if (!cb) {
|
277
|
+
throw `Unknown tool ${qualifiedToolName}`;
|
278
|
+
}
|
279
|
+
|
280
|
+
return cb(JSON.stringify(args));
|
281
|
+
}
|
282
|
+
|
283
|
+
/**
|
284
|
+
* "Settings" refers to the set of added servers and enabled tools.
|
285
|
+
*/
|
286
|
+
public getMcpServerSettings(): McpServerSettings {
|
287
|
+
const config: McpServerSettings = {};
|
288
|
+
|
289
|
+
// NOTE: on load, entires of the form:
|
290
|
+
//
|
291
|
+
// <server>: []
|
292
|
+
//
|
293
|
+
// may be interpreted as "all tools for <server>". If the client has left
|
294
|
+
// a server with no tools enabled, we mark it as disabled.
|
295
|
+
|
296
|
+
for (const [serverName, server] of Object.entries(this.mcpServers)) {
|
297
|
+
const tools = Object.keys(server.getEnabledTools());
|
298
|
+
if (tools.length > 0) {
|
299
|
+
config[serverName] = tools;
|
300
|
+
}
|
301
|
+
}
|
302
|
+
|
303
|
+
return config;
|
304
|
+
}
|
305
|
+
|
306
|
+
private getMcpServerInternal(mcpServerName: string): McpServerInfoInternal {
|
307
|
+
const server = this.mcpServers[mcpServerName];
|
308
|
+
if (server) {
|
309
|
+
return server;
|
310
|
+
}
|
311
|
+
throw Error(`unknown server ${mcpServerName}`);
|
312
|
+
}
|
313
|
+
}
|
314
|
+
|
315
|
+
export function computeQualifiedName(
|
316
|
+
mcpServerName: string,
|
317
|
+
toolName: string
|
318
|
+
): string {
|
319
|
+
return `${mcpServerName}__${toolName}`;
|
320
|
+
}
|
321
|
+
|
322
|
+
export function splitQualifiedName(
|
323
|
+
qualifiedToolName: string
|
324
|
+
): [string, string] {
|
325
|
+
const delimIdx = qualifiedToolName.indexOf("__");
|
326
|
+
if (delimIdx < 0) {
|
327
|
+
throw Error(`invalid qualified name: ${qualifiedToolName}`);
|
328
|
+
}
|
329
|
+
|
330
|
+
return [
|
331
|
+
qualifiedToolName.slice(0, delimIdx),
|
332
|
+
qualifiedToolName.slice(delimIdx + 2),
|
333
|
+
];
|
334
|
+
}
|
335
|
+
|
336
|
+
export function computeOpenAIToolList(mcpServers: {
|
337
|
+
[mcpServer: string]: McpServerInfoInternal;
|
338
|
+
}): ChatCompletionTool[] {
|
339
|
+
const openaiTools: ChatCompletionTool[] = [];
|
340
|
+
|
341
|
+
for (const mcpServerName in mcpServers) {
|
342
|
+
const mcpServer = mcpServers[mcpServerName];
|
343
|
+
const tools = mcpServer.getTools();
|
344
|
+
const enabled = mcpServer.getEnabledTools();
|
345
|
+
|
346
|
+
for (const mcpTool of tools) {
|
347
|
+
const toolName = mcpTool.name;
|
348
|
+
if (enabled[toolName]) {
|
349
|
+
const qualifiedName = computeQualifiedName(mcpServerName, toolName);
|
350
|
+
const openaiTool = mcpToolToOpenAITool(mcpTool, qualifiedName);
|
351
|
+
openaiTools.push(openaiTool);
|
352
|
+
}
|
353
|
+
}
|
354
|
+
}
|
355
|
+
|
356
|
+
return openaiTools;
|
357
|
+
}
|
358
|
+
|
359
|
+
export function mcpToolToOpenAITool(
|
360
|
+
tool: Tool,
|
361
|
+
qualifiedName?: string
|
362
|
+
): ChatCompletionTool {
|
363
|
+
return {
|
364
|
+
type: "function",
|
365
|
+
function: {
|
366
|
+
name: qualifiedName || tool.name,
|
367
|
+
description: tool.description,
|
368
|
+
parameters: tool.inputSchema,
|
369
|
+
},
|
370
|
+
};
|
371
|
+
}
|
@@ -0,0 +1,24 @@
|
|
1
|
+
import * as fs from "fs";
|
2
|
+
import { execSync } from "child_process";
|
3
|
+
import { IPlatform } from "./iplatform";
|
4
|
+
|
5
|
+
/**
|
6
|
+
* Implementation of the IPlatform interface for node.js
|
7
|
+
*/
|
8
|
+
export const NODE_PLATFORM: IPlatform = {
|
9
|
+
openUrl: (url: string) => {
|
10
|
+
const platform = process.platform;
|
11
|
+
if (platform === "darwin") {
|
12
|
+
execSync(`open '${url}'`);
|
13
|
+
} else if (platform === "linux") {
|
14
|
+
execSync(`xdg-open '${url}'`);
|
15
|
+
} else if (platform === "win32") {
|
16
|
+
execSync(`start '${url}'`);
|
17
|
+
} else {
|
18
|
+
throw `Unknown platform ${platform}`;
|
19
|
+
}
|
20
|
+
},
|
21
|
+
load: async (filename: string): Promise<string> => {
|
22
|
+
return fs.readFileSync(filename, { encoding: "utf8" });
|
23
|
+
},
|
24
|
+
};
|
package/src/openAILLM.ts
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
import { ILLM } from "./llm";
|
2
|
+
import { OpenAI } from "openai";
|
3
|
+
|
4
|
+
export class OpenAILLM implements ILLM {
|
5
|
+
private readonly openai: OpenAI;
|
6
|
+
private model: string;
|
7
|
+
|
8
|
+
constructor(
|
9
|
+
apiKey: string,
|
10
|
+
apiUrl: string | undefined,
|
11
|
+
model: string | undefined
|
12
|
+
) {
|
13
|
+
this.openai = new OpenAI({
|
14
|
+
apiKey,
|
15
|
+
baseURL: apiUrl,
|
16
|
+
dangerouslyAllowBrowser: true,
|
17
|
+
});
|
18
|
+
this.model = model || "gpt-4o-mini";
|
19
|
+
}
|
20
|
+
|
21
|
+
public setModel(model: string) {
|
22
|
+
this.model = model;
|
23
|
+
}
|
24
|
+
|
25
|
+
getModel(): string {
|
26
|
+
return this.model;
|
27
|
+
}
|
28
|
+
|
29
|
+
getUrl(): string {
|
30
|
+
return this.openai.baseURL;
|
31
|
+
}
|
32
|
+
|
33
|
+
public async getConversationResponse(
|
34
|
+
messages: OpenAI.Chat.Completions.ChatCompletionMessageParam[],
|
35
|
+
tools?: OpenAI.Chat.Completions.ChatCompletionTool[],
|
36
|
+
onMessage?: (msg: string, end: boolean) => Promise<void>
|
37
|
+
): Promise<OpenAI.Chat.Completions.ChatCompletion> {
|
38
|
+
const completion = await this.openai.chat.completions.create({
|
39
|
+
model: this.model,
|
40
|
+
messages,
|
41
|
+
tools,
|
42
|
+
});
|
43
|
+
if (onMessage) {
|
44
|
+
const message = completion.choices[0].message;
|
45
|
+
if (message.content) {
|
46
|
+
await onMessage(message.content, true);
|
47
|
+
}
|
48
|
+
}
|
49
|
+
return completion;
|
50
|
+
}
|
51
|
+
}
|