@xalia/agent 1.0.19
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 +57 -0
- package/dist/agent.js +278 -0
- package/dist/agentUtils.js +88 -0
- package/dist/chat.js +278 -0
- package/dist/dummyLLM.js +28 -0
- package/dist/files.js +115 -0
- package/dist/iplatform.js +2 -0
- package/dist/llm.js +2 -0
- package/dist/main.js +136 -0
- package/dist/mcpServerManager.js +269 -0
- package/dist/nodePlatform.js +61 -0
- package/dist/openAILLM.js +31 -0
- package/dist/options.js +79 -0
- package/dist/prompt.js +83 -0
- package/dist/sudoMcpServerManager.js +174 -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 +41 -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 +357 -0
- package/src/agentUtils.ts +188 -0
- package/src/chat.ts +325 -0
- package/src/dummyLLM.ts +36 -0
- package/src/files.ts +95 -0
- package/src/iplatform.ts +11 -0
- package/src/llm.ts +12 -0
- package/src/main.ts +171 -0
- package/src/mcpServerManager.ts +365 -0
- package/src/nodePlatform.ts +24 -0
- package/src/openAILLM.ts +43 -0
- package/src/options.ts +103 -0
- package/src/prompt.ts +93 -0
- package/src/sudoMcpServerManager.ts +268 -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 +63 -0
- package/src/tokenAuth.ts +50 -0
- package/src/tools.ts +57 -0
- package/test_data/background_test_profile.json +7 -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 +9 -0
- package/tsconfig.json +13 -0
@@ -0,0 +1,269 @@
|
|
1
|
+
"use strict";
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
+
exports.McpServerManager = exports.McpServerInfo = void 0;
|
4
|
+
exports.computeQualifiedName = computeQualifiedName;
|
5
|
+
exports.splitQualifiedName = splitQualifiedName;
|
6
|
+
exports.computeOpenAIToolList = computeOpenAIToolList;
|
7
|
+
exports.mcpToolToOpenAITool = mcpToolToOpenAITool;
|
8
|
+
const sse_js_1 = require("@modelcontextprotocol/sdk/client/sse.js");
|
9
|
+
const index_js_1 = require("@modelcontextprotocol/sdk/client/index.js");
|
10
|
+
const tokenAuth_1 = require("./tokenAuth");
|
11
|
+
const assert_1 = require("assert");
|
12
|
+
const sdk_1 = require("@xalia/xmcp/sdk");
|
13
|
+
const logger = (0, sdk_1.getLogger)();
|
14
|
+
/**
|
15
|
+
* The (read-only) McpServerInfo to expose to external classes. Callers
|
16
|
+
* should not modify this data directly. Only through the McpServerManager
|
17
|
+
* class.
|
18
|
+
*/
|
19
|
+
class McpServerInfo {
|
20
|
+
constructor(tools) {
|
21
|
+
const toolsMap = {};
|
22
|
+
for (const mcpTool of tools) {
|
23
|
+
const toolName = mcpTool.name;
|
24
|
+
toolsMap[toolName] = mcpTool;
|
25
|
+
}
|
26
|
+
this.tools = tools;
|
27
|
+
this.toolsMap = toolsMap;
|
28
|
+
this.enabledToolsMap = {};
|
29
|
+
}
|
30
|
+
getEnabledTools() {
|
31
|
+
return this.enabledToolsMap;
|
32
|
+
}
|
33
|
+
getTools() {
|
34
|
+
return this.tools;
|
35
|
+
}
|
36
|
+
getTool(toolName) {
|
37
|
+
return this.toolsMap[toolName];
|
38
|
+
}
|
39
|
+
}
|
40
|
+
exports.McpServerInfo = McpServerInfo;
|
41
|
+
/**
|
42
|
+
* The internal class holds server info and allows it to be updated. Managed
|
43
|
+
* by McpServerManager. Do not access these methods except via the
|
44
|
+
* McpServerManager.
|
45
|
+
*/
|
46
|
+
class McpServerInfoInternal extends McpServerInfo {
|
47
|
+
constructor(client, tools) {
|
48
|
+
super(tools);
|
49
|
+
const callbacks = {};
|
50
|
+
for (const mcpTool of tools) {
|
51
|
+
const toolName = mcpTool.name;
|
52
|
+
// Create callback
|
53
|
+
const callback = async (argStr) => {
|
54
|
+
logger.debug(`cb for ${toolName} invoked with args (${typeof argStr}): ` +
|
55
|
+
`${JSON.stringify(argStr)}`);
|
56
|
+
const argsObj = JSON.parse(argStr);
|
57
|
+
const toolResult = await client.callTool({
|
58
|
+
name: toolName,
|
59
|
+
arguments: argsObj,
|
60
|
+
});
|
61
|
+
logger.debug(`cb for ${toolName} returned: ${JSON.stringify(toolResult)}`);
|
62
|
+
(0, assert_1.strict)(typeof toolResult === "object");
|
63
|
+
const content = toolResult.content;
|
64
|
+
(0, assert_1.strict)(typeof content === "object");
|
65
|
+
(0, assert_1.strict)(content);
|
66
|
+
const content0 = content[0];
|
67
|
+
(0, assert_1.strict)(typeof content0 === "object");
|
68
|
+
const content0Text = content0.text;
|
69
|
+
(0, assert_1.strict)(typeof content0Text === "string");
|
70
|
+
return content0Text;
|
71
|
+
};
|
72
|
+
callbacks[toolName] = callback;
|
73
|
+
}
|
74
|
+
this.client = client;
|
75
|
+
this.callbacks = callbacks;
|
76
|
+
}
|
77
|
+
async shutdown() {
|
78
|
+
await this.client.close();
|
79
|
+
}
|
80
|
+
enableTool(toolName) {
|
81
|
+
this.enabledToolsMap[toolName] = true;
|
82
|
+
}
|
83
|
+
disableTool(toolName) {
|
84
|
+
delete this.enabledToolsMap[toolName];
|
85
|
+
}
|
86
|
+
getCallback(toolName) {
|
87
|
+
return this.callbacks[toolName];
|
88
|
+
}
|
89
|
+
}
|
90
|
+
/**
|
91
|
+
* Manage a set of MCP servers, where the tools for each server have an
|
92
|
+
* 'enabled' flag. Tools are disabled by default. The set of enabled tools
|
93
|
+
* over all servers is exposed as a single list of OpenAI functions.
|
94
|
+
*/
|
95
|
+
class McpServerManager {
|
96
|
+
constructor() {
|
97
|
+
this.mcpServers = {};
|
98
|
+
this.enabledToolsDirty = true;
|
99
|
+
this.enabledOpenAITools = [];
|
100
|
+
}
|
101
|
+
async shutdown() {
|
102
|
+
await Promise.all(Object.keys(this.mcpServers).map((name) => {
|
103
|
+
logger.debug(`shutting down: ${name}...`);
|
104
|
+
this.mcpServers[name].shutdown();
|
105
|
+
}));
|
106
|
+
this.mcpServers = {};
|
107
|
+
}
|
108
|
+
getMcpServerNames() {
|
109
|
+
return Object.keys(this.mcpServers);
|
110
|
+
}
|
111
|
+
getMcpServer(mcpServerName) {
|
112
|
+
return this.getMcpServerInternal(mcpServerName);
|
113
|
+
}
|
114
|
+
async addMcpServer(mcpServerName, url, apiKey, tools) {
|
115
|
+
logger.debug(`Adding mcp server ${mcpServerName}: ${url}`);
|
116
|
+
const sseTransportOptions = {};
|
117
|
+
if (apiKey) {
|
118
|
+
sseTransportOptions.authProvider = new tokenAuth_1.TokenAuth(apiKey);
|
119
|
+
}
|
120
|
+
const urlO = new URL(url);
|
121
|
+
const transport = new sse_js_1.SSEClientTransport(urlO, sseTransportOptions);
|
122
|
+
const client = new index_js_1.Client({
|
123
|
+
name: "@xalia/agent",
|
124
|
+
version: "1.0.0",
|
125
|
+
});
|
126
|
+
try {
|
127
|
+
await client.connect(transport);
|
128
|
+
}
|
129
|
+
catch (e) {
|
130
|
+
// TODO: is this catch necessary?
|
131
|
+
await client.close();
|
132
|
+
throw e;
|
133
|
+
}
|
134
|
+
await this.addMcpServerWithClient(client, mcpServerName, tools);
|
135
|
+
}
|
136
|
+
/**
|
137
|
+
* Add MCP server from an already connected McpClient.
|
138
|
+
*/
|
139
|
+
async addMcpServerWithClient(client, mcpServerName, tools) {
|
140
|
+
try {
|
141
|
+
// TODO; require the tools to be passed in.
|
142
|
+
if (!tools) {
|
143
|
+
const mcpTools = await client.listTools();
|
144
|
+
tools = mcpTools.tools;
|
145
|
+
}
|
146
|
+
this.mcpServers[mcpServerName] = new McpServerInfoInternal(client, tools);
|
147
|
+
}
|
148
|
+
catch (e) {
|
149
|
+
await client.close();
|
150
|
+
throw e;
|
151
|
+
}
|
152
|
+
}
|
153
|
+
async removeMcpServer(mcpServerName) {
|
154
|
+
const server = this.getMcpServerInternal(mcpServerName);
|
155
|
+
delete this.mcpServers[mcpServerName];
|
156
|
+
await server.shutdown();
|
157
|
+
this.enabledToolsDirty = true;
|
158
|
+
}
|
159
|
+
enableAllTools(mcpServerName) {
|
160
|
+
logger.debug(`enableAllTools: ${mcpServerName}`);
|
161
|
+
const server = this.getMcpServerInternal(mcpServerName);
|
162
|
+
for (const tool of server.getTools()) {
|
163
|
+
logger.debug(`enable: ${tool.name}`);
|
164
|
+
server.enableTool(tool.name);
|
165
|
+
}
|
166
|
+
this.enabledToolsDirty = true;
|
167
|
+
}
|
168
|
+
disableAllTools(mcpServerName) {
|
169
|
+
logger.debug(`disableAllTools: ${mcpServerName}`);
|
170
|
+
const server = this.getMcpServerInternal(mcpServerName);
|
171
|
+
for (const tool of server.getTools()) {
|
172
|
+
logger.debug(`disable: ${tool.name}`);
|
173
|
+
server.disableTool(tool.name);
|
174
|
+
}
|
175
|
+
this.enabledToolsDirty = true;
|
176
|
+
}
|
177
|
+
enableTool(mcpServerName, toolName) {
|
178
|
+
logger.debug(`enableTool: ${mcpServerName} ${toolName}`);
|
179
|
+
const server = this.getMcpServerInternal(mcpServerName);
|
180
|
+
server.enableTool(toolName);
|
181
|
+
this.enabledToolsDirty = true;
|
182
|
+
}
|
183
|
+
disableTool(mcpServerName, toolName) {
|
184
|
+
const server = this.getMcpServerInternal(mcpServerName);
|
185
|
+
server.disableTool(toolName);
|
186
|
+
this.enabledToolsDirty = true;
|
187
|
+
}
|
188
|
+
getOpenAITools() {
|
189
|
+
if (this.enabledToolsDirty) {
|
190
|
+
this.enabledOpenAITools = computeOpenAIToolList(this.mcpServers);
|
191
|
+
this.enabledToolsDirty = false;
|
192
|
+
}
|
193
|
+
return this.enabledOpenAITools;
|
194
|
+
}
|
195
|
+
/**
|
196
|
+
* Note the `qualifiedToolName` is the full `{mcpServerName}/{toolName}` as
|
197
|
+
* in the openai spec.
|
198
|
+
*/
|
199
|
+
async invoke(qualifiedToolName, args) {
|
200
|
+
const [mcpServerName, toolName] = splitQualifiedName(qualifiedToolName);
|
201
|
+
logger.debug(`invoke: qualified: ${qualifiedToolName}`);
|
202
|
+
logger.debug(`invoke: mcpServerName: ${mcpServerName}, toolName: ${toolName}`);
|
203
|
+
logger.debug(`invoke: args: ${JSON.stringify(args)}`);
|
204
|
+
const server = this.getMcpServerInternal(mcpServerName);
|
205
|
+
const cb = server.getCallback(toolName);
|
206
|
+
if (!cb) {
|
207
|
+
throw `Unknown tool ${qualifiedToolName}`;
|
208
|
+
}
|
209
|
+
return cb(JSON.stringify(args));
|
210
|
+
}
|
211
|
+
/**
|
212
|
+
* "Settings" refers to the set of added servers and enabled tools.
|
213
|
+
*/
|
214
|
+
getMcpServerSettings() {
|
215
|
+
const config = {};
|
216
|
+
for (const [serverName, server] of Object.entries(this.mcpServers)) {
|
217
|
+
config[serverName] = structuredClone(server.getEnabledTools());
|
218
|
+
}
|
219
|
+
return config;
|
220
|
+
}
|
221
|
+
getMcpServerInternal(mcpServerName) {
|
222
|
+
const server = this.mcpServers[mcpServerName];
|
223
|
+
if (server) {
|
224
|
+
return server;
|
225
|
+
}
|
226
|
+
throw Error(`unknown server ${mcpServerName}`);
|
227
|
+
}
|
228
|
+
}
|
229
|
+
exports.McpServerManager = McpServerManager;
|
230
|
+
function computeQualifiedName(mcpServerName, toolName) {
|
231
|
+
return `${mcpServerName}__${toolName}`;
|
232
|
+
}
|
233
|
+
function splitQualifiedName(qualifiedToolName) {
|
234
|
+
const delimIdx = qualifiedToolName.indexOf("__");
|
235
|
+
if (delimIdx < 0) {
|
236
|
+
throw Error(`invalid qualified name: ${qualifiedToolName}`);
|
237
|
+
}
|
238
|
+
return [
|
239
|
+
qualifiedToolName.slice(0, delimIdx),
|
240
|
+
qualifiedToolName.slice(delimIdx + 2),
|
241
|
+
];
|
242
|
+
}
|
243
|
+
function computeOpenAIToolList(mcpServers) {
|
244
|
+
const openaiTools = [];
|
245
|
+
for (const mcpServerName in mcpServers) {
|
246
|
+
const mcpServer = mcpServers[mcpServerName];
|
247
|
+
const tools = mcpServer.getTools();
|
248
|
+
const enabled = mcpServer.getEnabledTools();
|
249
|
+
for (const mcpTool of tools) {
|
250
|
+
const toolName = mcpTool.name;
|
251
|
+
if (enabled[toolName]) {
|
252
|
+
const qualifiedName = computeQualifiedName(mcpServerName, toolName);
|
253
|
+
const openaiTool = mcpToolToOpenAITool(mcpTool, qualifiedName);
|
254
|
+
openaiTools.push(openaiTool);
|
255
|
+
}
|
256
|
+
}
|
257
|
+
}
|
258
|
+
return openaiTools;
|
259
|
+
}
|
260
|
+
function mcpToolToOpenAITool(tool, qualifiedName) {
|
261
|
+
return {
|
262
|
+
type: "function",
|
263
|
+
function: {
|
264
|
+
name: qualifiedName || tool.name,
|
265
|
+
description: tool.description,
|
266
|
+
parameters: tool.inputSchema,
|
267
|
+
},
|
268
|
+
};
|
269
|
+
}
|
@@ -0,0 +1,61 @@
|
|
1
|
+
"use strict";
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
3
|
+
if (k2 === undefined) k2 = k;
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
7
|
+
}
|
8
|
+
Object.defineProperty(o, k2, desc);
|
9
|
+
}) : (function(o, m, k, k2) {
|
10
|
+
if (k2 === undefined) k2 = k;
|
11
|
+
o[k2] = m[k];
|
12
|
+
}));
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
15
|
+
}) : function(o, v) {
|
16
|
+
o["default"] = v;
|
17
|
+
});
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
19
|
+
var ownKeys = function(o) {
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
21
|
+
var ar = [];
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
23
|
+
return ar;
|
24
|
+
};
|
25
|
+
return ownKeys(o);
|
26
|
+
};
|
27
|
+
return function (mod) {
|
28
|
+
if (mod && mod.__esModule) return mod;
|
29
|
+
var result = {};
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
31
|
+
__setModuleDefault(result, mod);
|
32
|
+
return result;
|
33
|
+
};
|
34
|
+
})();
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
36
|
+
exports.NODE_PLATFORM = void 0;
|
37
|
+
const fs = __importStar(require("fs"));
|
38
|
+
const child_process_1 = require("child_process");
|
39
|
+
/**
|
40
|
+
* Implementation of the IPlatform interface for node.js
|
41
|
+
*/
|
42
|
+
exports.NODE_PLATFORM = {
|
43
|
+
openUrl: (url) => {
|
44
|
+
const platform = process.platform;
|
45
|
+
if (platform === "darwin") {
|
46
|
+
(0, child_process_1.execSync)(`open '${url}'`);
|
47
|
+
}
|
48
|
+
else if (platform === "linux") {
|
49
|
+
(0, child_process_1.execSync)(`xdg-open '${url}'`);
|
50
|
+
}
|
51
|
+
else if (platform === "win32") {
|
52
|
+
(0, child_process_1.execSync)(`start '${url}'`);
|
53
|
+
}
|
54
|
+
else {
|
55
|
+
throw `Unknown platform ${platform}`;
|
56
|
+
}
|
57
|
+
},
|
58
|
+
load: async (filename) => {
|
59
|
+
return fs.readFileSync(filename, { encoding: "utf8" });
|
60
|
+
},
|
61
|
+
};
|
@@ -0,0 +1,31 @@
|
|
1
|
+
"use strict";
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
+
exports.OpenAILLM = void 0;
|
4
|
+
const openai_1 = require("openai");
|
5
|
+
class OpenAILLM {
|
6
|
+
constructor(apiKey, apiUrl, model) {
|
7
|
+
this.openai = new openai_1.OpenAI({
|
8
|
+
apiKey,
|
9
|
+
baseURL: apiUrl,
|
10
|
+
dangerouslyAllowBrowser: true,
|
11
|
+
});
|
12
|
+
this.model = model || "gpt-4o-mini";
|
13
|
+
}
|
14
|
+
setModel(model) {
|
15
|
+
this.model = model;
|
16
|
+
}
|
17
|
+
getModel() {
|
18
|
+
return this.model;
|
19
|
+
}
|
20
|
+
getUrl() {
|
21
|
+
return this.openai.baseURL;
|
22
|
+
}
|
23
|
+
async getConversationResponse(messages, tools) {
|
24
|
+
return this.openai.chat.completions.create({
|
25
|
+
model: this.model,
|
26
|
+
messages,
|
27
|
+
tools,
|
28
|
+
});
|
29
|
+
}
|
30
|
+
}
|
31
|
+
exports.OpenAILLM = OpenAILLM;
|
package/dist/options.js
ADDED
@@ -0,0 +1,79 @@
|
|
1
|
+
"use strict";
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
+
exports.openaiApiUrl = exports.openaiApiKey = exports.approveToolsUpTo = exports.approveTools = exports.oneShot = exports.llmModel = exports.systemPromptFile = exports.imageFile = exports.promptFile = void 0;
|
4
|
+
exports.secretOption = secretOption;
|
5
|
+
const cmd_ts_1 = require("cmd-ts");
|
6
|
+
/// Prevents env content from being displayed in the help text.
|
7
|
+
function secretOption({ long, short, env, description, }) {
|
8
|
+
if (env) {
|
9
|
+
return (0, cmd_ts_1.option)({
|
10
|
+
type: (0, cmd_ts_1.optional)(cmd_ts_1.string),
|
11
|
+
long,
|
12
|
+
short,
|
13
|
+
description: `${description} [env: ${env}]`,
|
14
|
+
defaultValue: () => process.env[env],
|
15
|
+
defaultValueIsSerializable: false, // hides the value from --help
|
16
|
+
});
|
17
|
+
}
|
18
|
+
return (0, cmd_ts_1.option)({
|
19
|
+
type: (0, cmd_ts_1.optional)(cmd_ts_1.string),
|
20
|
+
long,
|
21
|
+
short,
|
22
|
+
description: `${description} (can also be set via ${env} env var)`,
|
23
|
+
});
|
24
|
+
}
|
25
|
+
exports.promptFile = (0, cmd_ts_1.option)({
|
26
|
+
type: (0, cmd_ts_1.optional)(cmd_ts_1.string),
|
27
|
+
long: "prompt",
|
28
|
+
short: "p",
|
29
|
+
description: "File containing user's first prompt to LLM",
|
30
|
+
});
|
31
|
+
exports.imageFile = (0, cmd_ts_1.option)({
|
32
|
+
type: (0, cmd_ts_1.optional)(cmd_ts_1.string),
|
33
|
+
long: "image",
|
34
|
+
short: "i",
|
35
|
+
description: "File containing image input",
|
36
|
+
});
|
37
|
+
exports.systemPromptFile = (0, cmd_ts_1.option)({
|
38
|
+
type: (0, cmd_ts_1.optional)(cmd_ts_1.string),
|
39
|
+
long: "sysprompt",
|
40
|
+
short: "s",
|
41
|
+
description: "File containing system prompt",
|
42
|
+
});
|
43
|
+
exports.llmModel = (0, cmd_ts_1.option)({
|
44
|
+
type: (0, cmd_ts_1.optional)(cmd_ts_1.string),
|
45
|
+
long: "model",
|
46
|
+
short: "m",
|
47
|
+
description: "LLM model",
|
48
|
+
env: "LLM_MODEL",
|
49
|
+
});
|
50
|
+
exports.oneShot = (0, cmd_ts_1.flag)({
|
51
|
+
type: cmd_ts_1.boolean,
|
52
|
+
long: "one-shot",
|
53
|
+
short: "1",
|
54
|
+
description: "Exit after first reply (implies --approve-tools)",
|
55
|
+
});
|
56
|
+
exports.approveTools = (0, cmd_ts_1.flag)({
|
57
|
+
type: cmd_ts_1.boolean,
|
58
|
+
long: "approve-tools",
|
59
|
+
short: "y",
|
60
|
+
description: "Automatically approve all tool calls",
|
61
|
+
});
|
62
|
+
exports.approveToolsUpTo = (0, cmd_ts_1.option)({
|
63
|
+
type: (0, cmd_ts_1.optional)(cmd_ts_1.number),
|
64
|
+
long: "approve-tools-up-to",
|
65
|
+
description: "Automatically approve all tool calls up to some number",
|
66
|
+
});
|
67
|
+
exports.openaiApiKey = secretOption({
|
68
|
+
long: "openai-api-key",
|
69
|
+
short: "o",
|
70
|
+
description: "OpenAI (or compatible protocol) API Key",
|
71
|
+
env: "OPENAI_API_KEY",
|
72
|
+
});
|
73
|
+
exports.openaiApiUrl = (0, cmd_ts_1.option)({
|
74
|
+
type: (0, cmd_ts_1.optional)(cmd_ts_1.string),
|
75
|
+
long: "openai-api-url",
|
76
|
+
short: "u",
|
77
|
+
description: "OpenAI (or compatible protocol) RPC endpoint url",
|
78
|
+
env: "OPENAI_API_URL",
|
79
|
+
});
|
package/dist/prompt.js
ADDED
@@ -0,0 +1,83 @@
|
|
1
|
+
"use strict";
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
4
|
+
};
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
6
|
+
exports.Prompt = void 0;
|
7
|
+
exports.parsePrompt = parsePrompt;
|
8
|
+
const readline_1 = __importDefault(require("readline"));
|
9
|
+
const DEFAULT_PROMPT = "USER: ";
|
10
|
+
class Prompt {
|
11
|
+
constructor() {
|
12
|
+
this.prompt = readline_1.default.createInterface({
|
13
|
+
input: process.stdin,
|
14
|
+
output: process.stdout,
|
15
|
+
prompt: DEFAULT_PROMPT,
|
16
|
+
});
|
17
|
+
this.prompt.on("line", (line) => {
|
18
|
+
this.line = line;
|
19
|
+
this.resolve();
|
20
|
+
});
|
21
|
+
this.prompt.on("close", () => {
|
22
|
+
this.line = undefined;
|
23
|
+
this.resolve();
|
24
|
+
});
|
25
|
+
}
|
26
|
+
async run(prompt) {
|
27
|
+
// Clear any line
|
28
|
+
this.line = "";
|
29
|
+
return new Promise((r) => {
|
30
|
+
this.online = r;
|
31
|
+
if (prompt) {
|
32
|
+
this.prompt.setPrompt(prompt);
|
33
|
+
}
|
34
|
+
this.prompt.prompt();
|
35
|
+
if (prompt) {
|
36
|
+
this.prompt.setPrompt(DEFAULT_PROMPT);
|
37
|
+
}
|
38
|
+
});
|
39
|
+
}
|
40
|
+
shutdown() {
|
41
|
+
this.prompt.close();
|
42
|
+
}
|
43
|
+
resolve() {
|
44
|
+
if (this.online) {
|
45
|
+
this.online(this.line);
|
46
|
+
}
|
47
|
+
this.online = undefined;
|
48
|
+
}
|
49
|
+
}
|
50
|
+
exports.Prompt = Prompt;
|
51
|
+
/**
|
52
|
+
* Support prompts:
|
53
|
+
* - some text (msg: some text, cmds: undefined)
|
54
|
+
* - :i image.png some text (msg: some text, cmds: ["i", "image.png"])
|
55
|
+
* - :i image.png (msg: undefined, cmds: ["i", "image.png"])
|
56
|
+
* - :l (msg: undefined, cmds: ["l"])
|
57
|
+
* - :e toolName .. (msg: undefined, cmds: ["e", "toolName", ...])
|
58
|
+
* - :ea toolName .. (msg: undefined, cmds: ["ea", "toolName", ...])
|
59
|
+
*/
|
60
|
+
function parsePrompt(prompt) {
|
61
|
+
prompt = prompt.trim();
|
62
|
+
let msg = undefined;
|
63
|
+
let cmds = undefined;
|
64
|
+
if (prompt.startsWith(":") || prompt.startsWith("/")) {
|
65
|
+
cmds = prompt.split(" ");
|
66
|
+
cmds[0] = cmds[0].slice(1);
|
67
|
+
if (cmds[0] == "i") {
|
68
|
+
// :i is special as it may have a trailing message
|
69
|
+
const fileDelim = prompt.indexOf(" ", 3);
|
70
|
+
if (fileDelim < 0) {
|
71
|
+
cmds = [cmds[0], prompt.slice(3)];
|
72
|
+
}
|
73
|
+
else {
|
74
|
+
msg = prompt.slice(fileDelim + 1);
|
75
|
+
cmds = [cmds[0], prompt.slice(3, fileDelim)];
|
76
|
+
}
|
77
|
+
}
|
78
|
+
}
|
79
|
+
else {
|
80
|
+
msg = prompt;
|
81
|
+
}
|
82
|
+
return { msg, cmds };
|
83
|
+
}
|
@@ -0,0 +1,174 @@
|
|
1
|
+
"use strict";
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
+
exports.SudoMcpServerManager = exports.LOCAL_SERVER_URL = void 0;
|
4
|
+
const sdk_1 = require("@xalia/xmcp/sdk");
|
5
|
+
const assert_1 = require("assert");
|
6
|
+
const index_js_1 = require("@modelcontextprotocol/sdk/client/index.js");
|
7
|
+
const logger = (0, sdk_1.getLogger)();
|
8
|
+
exports.LOCAL_SERVER_URL = "http://localhost:5001";
|
9
|
+
/**
|
10
|
+
* ServerBrief with sanitized name, and original name in another field.
|
11
|
+
*/
|
12
|
+
class SanitizedServerBrief extends sdk_1.McpServerBrief {
|
13
|
+
constructor() {
|
14
|
+
super("dummy", "dummy", "dummy", false, {}, "dummy", "dummy");
|
15
|
+
this.originalName = "dummy";
|
16
|
+
}
|
17
|
+
static fromServerBrief(brief) {
|
18
|
+
const b = brief;
|
19
|
+
const origName = brief.name;
|
20
|
+
b.originalName = origName; // eslint-disable-line
|
21
|
+
brief.name = sanitizeName(origName);
|
22
|
+
return b;
|
23
|
+
}
|
24
|
+
}
|
25
|
+
/**
|
26
|
+
* Manages access to the catalogue of servers hosted by sudomcp. Supports
|
27
|
+
* adding these servers to McpServerManager.
|
28
|
+
*/
|
29
|
+
class SudoMcpServerManager {
|
30
|
+
constructor(mcpServerManager, apiClient, serverBriefs, serverBriefsMap, toolCache, openUrl,
|
31
|
+
// Redirect to this page after successful authorization
|
32
|
+
authorized_url) {
|
33
|
+
this.mcpServerManager = mcpServerManager;
|
34
|
+
this.apiClient = apiClient;
|
35
|
+
this.serverBriefs = serverBriefs;
|
36
|
+
this.serverBriefsMap = serverBriefsMap;
|
37
|
+
this.toolCache = toolCache;
|
38
|
+
this.openUrl = openUrl;
|
39
|
+
this.authorized_url = authorized_url;
|
40
|
+
}
|
41
|
+
/**
|
42
|
+
* Initialize an ApiClient to interface with SudoMCP backend and
|
43
|
+
* fetch the current list of ServerBriefs.
|
44
|
+
*/
|
45
|
+
static async initialize(mcpServerManager, openUrl, sudoMcpUrl, sudoMcpApiKey, authorized_url) {
|
46
|
+
// TODO: Keep it on here and pass to `McpServerManager.addMcpServer`
|
47
|
+
const apiClient = new sdk_1.ApiClient(sudoMcpUrl ?? sdk_1.DEFAULT_SERVER_URL, sudoMcpApiKey ?? "dummy_key");
|
48
|
+
// Fetch server list
|
49
|
+
const servers = await apiClient.search([]);
|
50
|
+
const [mcpServers, mcpServersMap] = buildServersList(servers);
|
51
|
+
return new SudoMcpServerManager(mcpServerManager, apiClient, mcpServers, mcpServersMap, {}, openUrl, authorized_url);
|
52
|
+
}
|
53
|
+
/// TODO: Bit awkward that we have to restore via this class, but it's the
|
54
|
+
/// only class which knows how to restore (restart) the mcp servers.
|
55
|
+
async restoreMcpSettings(mcpSettings, serverConfigs) {
|
56
|
+
await this.restoreConfiguration(mcpSettings, serverConfigs);
|
57
|
+
}
|
58
|
+
/**
|
59
|
+
* Load the configuration from sudomcp, and enable the specified tools.
|
60
|
+
*/
|
61
|
+
async restoreConfiguration(mcpConfig, serverConfigs) {
|
62
|
+
logger.debug("Restoring Mcp config");
|
63
|
+
// TODO: remove existing servers?
|
64
|
+
for (const [serverName, enabled] of Object.entries(mcpConfig)) {
|
65
|
+
logger.debug(` restoring "${serverName}" ...`);
|
66
|
+
if (Object.keys(enabled).length === 0) {
|
67
|
+
logger.debug(` restoring "${serverName}": (empty)`);
|
68
|
+
continue;
|
69
|
+
}
|
70
|
+
const serverConfig = serverConfigs[serverName] ?? {};
|
71
|
+
logger.debug(` restoring ${serverName}: ${JSON.stringify(Object.keys(enabled))}`);
|
72
|
+
await this.addMcpServer(serverName, serverConfig);
|
73
|
+
for (const [toolName, v] of Object.entries(enabled)) {
|
74
|
+
(0, assert_1.strict)(v === true);
|
75
|
+
this.mcpServerManager.enableTool(serverName, toolName);
|
76
|
+
}
|
77
|
+
}
|
78
|
+
}
|
79
|
+
/**
|
80
|
+
* Query backend for server list, clear tool cache.
|
81
|
+
*/
|
82
|
+
async refresh() {
|
83
|
+
const servers = await this.apiClient.search([]);
|
84
|
+
const [mcpServers, mcpServersMap] = buildServersList(servers);
|
85
|
+
this.serverBriefs = mcpServers;
|
86
|
+
this.serverBriefsMap = mcpServersMap;
|
87
|
+
this.toolCache = {};
|
88
|
+
}
|
89
|
+
getServerBriefs() {
|
90
|
+
return this.serverBriefs;
|
91
|
+
}
|
92
|
+
getMcpServerManager() {
|
93
|
+
return this.mcpServerManager;
|
94
|
+
}
|
95
|
+
/**
|
96
|
+
* Return tool list for a given MCP server. Queries the backend
|
97
|
+
* if necessary and caches the result.
|
98
|
+
*/
|
99
|
+
async getServerTools(serverName) {
|
100
|
+
// Check cache
|
101
|
+
let tools = this.toolCache[serverName];
|
102
|
+
if (tools) {
|
103
|
+
return tools;
|
104
|
+
}
|
105
|
+
// Query backend (using the original name)
|
106
|
+
const originalName = this.serverBriefsMap[serverName].originalName;
|
107
|
+
tools = await this.apiClient.listTools(originalName);
|
108
|
+
this.toolCache[serverName] = tools;
|
109
|
+
return tools;
|
110
|
+
}
|
111
|
+
/**
|
112
|
+
* Add a server to the `McpServerManager`, using `ApiClient`
|
113
|
+
* to produce the transport. Validates the server's config
|
114
|
+
* schema, if applicable.
|
115
|
+
*/
|
116
|
+
async addMcpServer(serverName, configuration) {
|
117
|
+
const tools = await this.getServerTools(serverName);
|
118
|
+
const originalName = this.serverBriefsMap[serverName].originalName;
|
119
|
+
const mcpserver = await this.apiClient.getDetails(originalName, "run");
|
120
|
+
(0, sdk_1.verifyConfigForServer)(mcpserver, configuration);
|
121
|
+
const client = new index_js_1.Client({
|
122
|
+
name: "@xalia/agent",
|
123
|
+
version: "1.0.0",
|
124
|
+
});
|
125
|
+
await connectServer(client, this.apiClient, mcpserver, configuration, this.openUrl, this.authorized_url);
|
126
|
+
await this.mcpServerManager.addMcpServerWithClient(client, serverName, tools);
|
127
|
+
}
|
128
|
+
getOriginalName(serverName) {
|
129
|
+
return this.serverBriefsMap[serverName].name;
|
130
|
+
}
|
131
|
+
}
|
132
|
+
exports.SudoMcpServerManager = SudoMcpServerManager;
|
133
|
+
/**
|
134
|
+
* Connect a client to a hosted MCP server session,
|
135
|
+
* prompting for authentication if needed.
|
136
|
+
*/
|
137
|
+
async function connectServer(client, apiClient, mcpServer, config, openUrl, authorized_url, noRetry = false) {
|
138
|
+
const transport = apiClient.mcpSession(mcpServer, config ?? {});
|
139
|
+
await client.connect(transport).catch(async (e) => {
|
140
|
+
if (e instanceof sdk_1.AuthenticationRequired && !noRetry) {
|
141
|
+
logger.info("authentication required: " + e.msg);
|
142
|
+
const { url, authenticatedP } = await apiClient.authenticate(mcpServer.name, authorized_url);
|
143
|
+
logger.info(`authenticate at url: ${url}`);
|
144
|
+
openUrl(url);
|
145
|
+
const authResult = await authenticatedP;
|
146
|
+
logger.info(`authResult: ${authResult}`);
|
147
|
+
if (!authResult) {
|
148
|
+
throw "authentication failed";
|
149
|
+
}
|
150
|
+
return connectServer(client, apiClient, mcpServer, config, openUrl, authorized_url, true);
|
151
|
+
}
|
152
|
+
else {
|
153
|
+
throw e;
|
154
|
+
}
|
155
|
+
});
|
156
|
+
}
|
157
|
+
/**
|
158
|
+
* Given a list of`ServerBrief` objects, create the corresponding list and map
|
159
|
+
* of `SantiizedServerBrief` objects (namely, objects with sanitized names,
|
160
|
+
* holding the original name in a new field).
|
161
|
+
*/
|
162
|
+
function buildServersList(serverList) {
|
163
|
+
const servers = [];
|
164
|
+
const serversMap = {};
|
165
|
+
for (const s of serverList) {
|
166
|
+
const ss = SanitizedServerBrief.fromServerBrief(s);
|
167
|
+
servers.push(ss);
|
168
|
+
serversMap[ss.name] = ss;
|
169
|
+
}
|
170
|
+
return [servers, serversMap];
|
171
|
+
}
|
172
|
+
function sanitizeName(name) {
|
173
|
+
return name.replace("/", "_");
|
174
|
+
}
|