@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.
Files changed (64) hide show
  1. package/.prettierrc.json +11 -0
  2. package/README.md +57 -0
  3. package/dist/agent.js +278 -0
  4. package/dist/agentUtils.js +88 -0
  5. package/dist/chat.js +278 -0
  6. package/dist/dummyLLM.js +28 -0
  7. package/dist/files.js +115 -0
  8. package/dist/iplatform.js +2 -0
  9. package/dist/llm.js +2 -0
  10. package/dist/main.js +136 -0
  11. package/dist/mcpServerManager.js +269 -0
  12. package/dist/nodePlatform.js +61 -0
  13. package/dist/openAILLM.js +31 -0
  14. package/dist/options.js +79 -0
  15. package/dist/prompt.js +83 -0
  16. package/dist/sudoMcpServerManager.js +174 -0
  17. package/dist/test/imageLoad.test.js +14 -0
  18. package/dist/test/mcpServerManager.test.js +71 -0
  19. package/dist/test/prompt.test.js +26 -0
  20. package/dist/test/sudoMcpServerManager.test.js +49 -0
  21. package/dist/tokenAuth.js +39 -0
  22. package/dist/tools.js +44 -0
  23. package/eslint.config.mjs +25 -0
  24. package/frog.png +0 -0
  25. package/package.json +41 -0
  26. package/scripts/git_message +31 -0
  27. package/scripts/git_wip +21 -0
  28. package/scripts/pr_message +18 -0
  29. package/scripts/pr_review +16 -0
  30. package/scripts/sudomcp_import +23 -0
  31. package/scripts/test_script +60 -0
  32. package/src/agent.ts +357 -0
  33. package/src/agentUtils.ts +188 -0
  34. package/src/chat.ts +325 -0
  35. package/src/dummyLLM.ts +36 -0
  36. package/src/files.ts +95 -0
  37. package/src/iplatform.ts +11 -0
  38. package/src/llm.ts +12 -0
  39. package/src/main.ts +171 -0
  40. package/src/mcpServerManager.ts +365 -0
  41. package/src/nodePlatform.ts +24 -0
  42. package/src/openAILLM.ts +43 -0
  43. package/src/options.ts +103 -0
  44. package/src/prompt.ts +93 -0
  45. package/src/sudoMcpServerManager.ts +268 -0
  46. package/src/test/imageLoad.test.ts +14 -0
  47. package/src/test/mcpServerManager.test.ts +98 -0
  48. package/src/test/prompt.test.src +0 -0
  49. package/src/test/prompt.test.ts +26 -0
  50. package/src/test/sudoMcpServerManager.test.ts +63 -0
  51. package/src/tokenAuth.ts +50 -0
  52. package/src/tools.ts +57 -0
  53. package/test_data/background_test_profile.json +7 -0
  54. package/test_data/background_test_script.json +11 -0
  55. package/test_data/dummyllm_script_simplecalc.json +28 -0
  56. package/test_data/git_message_profile.json +4 -0
  57. package/test_data/git_wip_system.txt +5 -0
  58. package/test_data/pr_message_profile.json +4 -0
  59. package/test_data/pr_review_profile.json +4 -0
  60. package/test_data/prompt_simplecalc.txt +1 -0
  61. package/test_data/simplecalc_profile.json +4 -0
  62. package/test_data/sudomcp_import_profile.json +4 -0
  63. package/test_data/test_script_profile.json +9 -0
  64. 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;
@@ -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
+ }