wxo-builder-mcp-server 1.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/agents.js ADDED
@@ -0,0 +1,448 @@
1
+ import { woFetch } from './auth.js';
2
+ import { getDefaultModelId } from './models.js';
3
+ const TEST_AGENT_NAME = 'WxoBuilderTestAgent';
4
+ /**
5
+ * List all available agents.
6
+ * Uses same API as vscode-extension: GET /v1/orchestrate/agents
7
+ */
8
+ export async function listAgents(limit = 20, offset = 0) {
9
+ console.error(`Listing agents...`);
10
+ try {
11
+ const response = await woFetch(`/v1/orchestrate/agents?limit=${limit}&offset=${offset}`, {
12
+ method: 'GET',
13
+ });
14
+ if (!response.ok) {
15
+ const text = await response.text();
16
+ throw new Error(`Failed to list agents: ${response.status} ${response.statusText} - ${text}`);
17
+ }
18
+ const text = await response.text();
19
+ try {
20
+ return JSON.parse(text);
21
+ }
22
+ catch {
23
+ return text;
24
+ }
25
+ }
26
+ catch (e) {
27
+ throw new Error(`API Request Failed: ${e.message}`);
28
+ }
29
+ }
30
+ /**
31
+ * Get chat starter settings (welcome message, quick prompts) for an agent.
32
+ */
33
+ export async function getChatStarterSettings(agentId) {
34
+ try {
35
+ const res = await woFetch(`/v1/orchestrate/agents/${agentId}/chat-starter-settings`, { method: 'GET' });
36
+ if (!res.ok) {
37
+ if (res.status === 404)
38
+ return { starter_prompts: { prompts: [] }, welcome_content: {} };
39
+ throw new Error(`Failed to get chat starter settings: ${res.status}`);
40
+ }
41
+ return await res.json();
42
+ }
43
+ catch (e) {
44
+ throw new Error(`API Request Failed: ${e.message}`);
45
+ }
46
+ }
47
+ /**
48
+ * Update chat starter settings for an agent.
49
+ */
50
+ export async function updateChatStarterSettings(agentId, payload) {
51
+ const res = await woFetch(`/v1/orchestrate/agents/${agentId}/chat-starter-settings`, {
52
+ method: 'PUT',
53
+ body: JSON.stringify(payload),
54
+ });
55
+ if (!res.ok) {
56
+ const text = await res.text();
57
+ throw new Error(`Failed to update chat starter settings: ${res.status} ${text}`);
58
+ }
59
+ return { success: true };
60
+ }
61
+ /**
62
+ * Get a specific agent by ID.
63
+ */
64
+ export async function getAgent(agentId) {
65
+ console.error(`Getting agent ${agentId}...`);
66
+ try {
67
+ const response = await woFetch(`/v1/orchestrate/agents/${agentId}`, { method: 'GET' });
68
+ if (!response.ok) {
69
+ throw new Error(`Failed to get agent: ${response.status} ${response.statusText}`);
70
+ }
71
+ const text = await response.text();
72
+ try {
73
+ return JSON.parse(text);
74
+ }
75
+ catch {
76
+ return text;
77
+ }
78
+ }
79
+ catch (e) {
80
+ throw new Error(`API Request Failed: ${e.message}`);
81
+ }
82
+ }
83
+ /**
84
+ * Create a new Agent.
85
+ * @param name Name of the agent
86
+ * @param description Description
87
+ * @param modelId The LLM model ID
88
+ * @param instructions System prompt
89
+ * @param tools Optional array of tool/skill IDs to assign to the agent
90
+ */
91
+ export async function createAgent(name, description, modelId, instructions, tools) {
92
+ console.error(`Creating agent "${name}"...`);
93
+ const payload = {
94
+ name,
95
+ description,
96
+ agent_type: 'watsonx',
97
+ llm: modelId,
98
+ instructions,
99
+ style: 'default',
100
+ settings: {},
101
+ };
102
+ try {
103
+ const response = await woFetch('/v1/orchestrate/agents', {
104
+ method: 'POST',
105
+ body: JSON.stringify(payload),
106
+ });
107
+ if (!response.ok) {
108
+ const text = await response.text();
109
+ throw new Error(`Failed to create agent: ${response.status} ${response.statusText} - ${text}`);
110
+ }
111
+ const text = await response.text();
112
+ let result;
113
+ try {
114
+ result = JSON.parse(text);
115
+ }
116
+ catch {
117
+ result = text;
118
+ }
119
+ // If tools provided, update agent to assign them
120
+ const agentId = result?.id ?? result?.data?.id ?? result;
121
+ if (tools && tools.length > 0 && agentId) {
122
+ await updateAgent(agentId, { tools });
123
+ result.tools = tools;
124
+ }
125
+ return result;
126
+ }
127
+ catch (e) {
128
+ throw new Error(`API Request Failed: ${e.message}`);
129
+ }
130
+ }
131
+ /**
132
+ * Update an agent by name or ID. Resolves agent_name to agent_id if needed.
133
+ * If payload contains welcome_message or quick_prompts, also updates chat starter settings.
134
+ */
135
+ export async function updateAgentByNameOrId(args) {
136
+ const { agent_id, agent_name, payload } = args;
137
+ let agentId = agent_id;
138
+ if (!agentId && agent_name) {
139
+ const resolved = await resolveAgentByName(agent_name);
140
+ if (!resolved)
141
+ throw new Error(`Agent not found: "${agent_name}"`);
142
+ agentId = resolved;
143
+ }
144
+ if (!agentId)
145
+ throw new Error('Provide agent_id or agent_name');
146
+ const { welcome_message, quick_prompts, ...agentPayload } = payload;
147
+ await updateAgent(agentId, agentPayload);
148
+ if (welcome_message !== undefined || quick_prompts !== undefined) {
149
+ const chatPayload = {};
150
+ if (welcome_message !== undefined)
151
+ chatPayload.welcome_content = { welcome_message: welcome_message || null };
152
+ if (quick_prompts !== undefined)
153
+ chatPayload.starter_prompts = { customize: quick_prompts };
154
+ await updateChatStarterSettings(agentId, chatPayload);
155
+ }
156
+ return { success: true };
157
+ }
158
+ /**
159
+ * Update an agent (e.g. assign tools, change model, instructions).
160
+ */
161
+ export async function updateAgent(agentId, payload) {
162
+ console.error(`Updating agent ${agentId}...`);
163
+ const response = await woFetch(`/v1/orchestrate/agents/${agentId}`, {
164
+ method: 'PATCH',
165
+ body: JSON.stringify(payload),
166
+ });
167
+ if (!response.ok) {
168
+ const text = await response.text();
169
+ throw new Error(`Failed to update agent: ${response.status} ${text}`);
170
+ }
171
+ const text = await response.text();
172
+ try {
173
+ return JSON.parse(text);
174
+ }
175
+ catch {
176
+ return { success: true };
177
+ }
178
+ }
179
+ /**
180
+ * Delete an agent by ID.
181
+ */
182
+ export async function deleteAgent(agentId) {
183
+ console.error(`Deleting agent ${agentId}...`);
184
+ const response = await woFetch(`/v1/orchestrate/agents/${agentId}`, { method: 'DELETE' });
185
+ if (!response.ok) {
186
+ const text = await response.text();
187
+ throw new Error(`Failed to delete agent: ${response.status} ${text}`);
188
+ }
189
+ return { success: true };
190
+ }
191
+ /**
192
+ * Resolve agent name to agent ID. Matches by name or display_name (case-insensitive, partial match).
193
+ */
194
+ export async function resolveAgentByName(agentName) {
195
+ const data = await listAgents(100, 0);
196
+ let agents = [];
197
+ if (Array.isArray(data))
198
+ agents = data;
199
+ else if (data?.assistants)
200
+ agents = data.assistants;
201
+ else if (data?.data)
202
+ agents = data.data;
203
+ const nameLower = agentName.toLowerCase().trim();
204
+ const match = agents.find((a) => (a.name || '').toLowerCase() === nameLower ||
205
+ (a.display_name || '').toLowerCase() === nameLower ||
206
+ (a.name || '').toLowerCase().includes(nameLower) ||
207
+ (a.display_name || '').toLowerCase().includes(nameLower));
208
+ return match ? (match.id ?? null) : null;
209
+ }
210
+ /**
211
+ * List tools assigned to an agent, with display names. Use agent_id or agent_name.
212
+ * For queries like "which tools are assigned to TimeWeatherAgent".
213
+ */
214
+ export async function listAgentTools(args) {
215
+ const { listSkills } = await import('./skills.js');
216
+ let agentId = args.agent_id;
217
+ let agentName = '';
218
+ if (!agentId && args.agent_name) {
219
+ const resolved = await resolveAgentByName(args.agent_name);
220
+ if (!resolved)
221
+ throw new Error(`Agent not found: "${args.agent_name}"`);
222
+ agentId = resolved;
223
+ agentName = args.agent_name;
224
+ }
225
+ if (!agentId)
226
+ throw new Error('Provide agent_id or agent_name');
227
+ const agent = await getAgent(agentId);
228
+ agentName = agent?.name || agent?.display_name || agentName || agentId;
229
+ const rawTools = agent?.tools ?? agent?.skill_ids ?? agent?.skills ?? [];
230
+ const toolIds = Array.isArray(rawTools)
231
+ ? rawTools
232
+ .map((t) => (typeof t === 'string' ? t : t?.id))
233
+ .filter((id) => typeof id === 'string' && id.length > 0)
234
+ : [];
235
+ const skillsRaw = await listSkills(100, 0);
236
+ const skillsList = Array.isArray(skillsRaw)
237
+ ? skillsRaw
238
+ : skillsRaw?.items ||
239
+ skillsRaw?.tools ||
240
+ skillsRaw?.data ||
241
+ [];
242
+ const idToMeta = new Map();
243
+ for (const s of skillsList) {
244
+ const id = s.id || s.name;
245
+ if (id)
246
+ idToMeta.set(id, { display_name: s.display_name || s.name || id, description: s.description });
247
+ }
248
+ const toolsWithNames = toolIds.map((id) => {
249
+ const meta = idToMeta.get(id);
250
+ return {
251
+ id,
252
+ display_name: meta?.display_name || id,
253
+ ...(meta?.description ? { description: meta.description } : {}),
254
+ };
255
+ });
256
+ return { agent_id: agentId, agent_name: agentName, tools: toolsWithNames };
257
+ }
258
+ /**
259
+ * Assign a tool to an agent (add to existing tools). Use tool_name or tool_id, agent_name or agent_id.
260
+ * For "assign REST Countries tool to TimeWeatherAgent" – use MCP, not ADK.
261
+ */
262
+ export async function assignToolToAgent(args) {
263
+ const { resolveToolByName } = await import('./skills.js');
264
+ let toolId = args.tool_id;
265
+ if (!toolId && args.tool_name) {
266
+ const resolved = await resolveToolByName(args.tool_name);
267
+ if (!resolved)
268
+ throw new Error(`Tool not found: "${args.tool_name}"`);
269
+ toolId = resolved;
270
+ }
271
+ if (!toolId)
272
+ throw new Error('Provide tool_id or tool_name');
273
+ let agentId = args.agent_id;
274
+ if (!agentId && args.agent_name) {
275
+ const resolved = await resolveAgentByName(args.agent_name);
276
+ if (!resolved)
277
+ throw new Error(`Agent not found: "${args.agent_name}"`);
278
+ agentId = resolved;
279
+ }
280
+ if (!agentId)
281
+ throw new Error('Provide agent_id or agent_name');
282
+ const agent = await getAgent(agentId);
283
+ const agentName = agent?.name || agent?.display_name || agentId;
284
+ const rawTools = agent?.tools ?? agent?.skill_ids ?? agent?.skills ?? [];
285
+ const existingIds = Array.isArray(rawTools)
286
+ ? rawTools
287
+ .map((t) => (typeof t === 'string' ? t : t?.id))
288
+ .filter((id) => typeof id === 'string' && id.length > 0)
289
+ : [];
290
+ if (existingIds.includes(toolId)) {
291
+ return { success: true, agent_id: agentId, agent_name: agentName, tools: existingIds };
292
+ }
293
+ const newTools = [...existingIds, toolId];
294
+ await updateAgent(agentId, { tools: newTools });
295
+ return { success: true, agent_id: agentId, agent_name: agentName, tools: newTools };
296
+ }
297
+ /**
298
+ * Ensure the WxoBuilderTestAgent exists and has the given tool assigned.
299
+ * Creates the agent if missing, then updates it to have this tool.
300
+ * Returns the agent ID for use in remote tool runs.
301
+ */
302
+ export async function ensureTestAgentForTool(toolId) {
303
+ let agents = [];
304
+ const data = await listAgents(100, 0);
305
+ if (Array.isArray(data)) {
306
+ agents = data;
307
+ }
308
+ else if (data?.assistants && Array.isArray(data.assistants)) {
309
+ agents = data.assistants;
310
+ }
311
+ else if (data?.data && Array.isArray(data.data)) {
312
+ agents = data.data;
313
+ }
314
+ const agent = agents.find((a) => (a.name || a.display_name) === TEST_AGENT_NAME);
315
+ if (!agent) {
316
+ const defaultLlm = await getDefaultModelId();
317
+ const created = await createAgent(TEST_AGENT_NAME, 'WxO Builder internal agent for remote tool testing. Do not delete.', defaultLlm, 'When the user asks you to execute a tool, execute it and return the raw result. Do not add commentary.', [toolId]);
318
+ const agentId = (created?.data?.id ?? created?.id ?? created);
319
+ if (agentId)
320
+ await updateAgent(agentId, { tools: [toolId] });
321
+ return agentId;
322
+ }
323
+ const agentId = agent.id || agent;
324
+ await updateAgent(agentId, { tools: [toolId] });
325
+ return agentId;
326
+ }
327
+ /**
328
+ * Update agent instructions based on assigned tools. Fetches tools, builds instructions from names/descriptions, and patches the agent.
329
+ */
330
+ export async function updateAgentInstructionsFromTools(args) {
331
+ const toolsResult = await listAgentTools(args);
332
+ const { agent_id, agent_name, tools } = toolsResult;
333
+ const knownPurposes = {
334
+ 'World Time': 'Get current time for any timezone (e.g. Europe/Amsterdam, America/New_York)',
335
+ 'Dad Jokes Skill': 'Tell random dad jokes',
336
+ 'mvk-weatherv4': 'Get current weather for locations worldwide',
337
+ 'REST Countries': 'Look up country data: population, area, capital, flags, languages',
338
+ 'Aviation Weather METAR': 'Get METAR weather reports for airport ICAO codes',
339
+ 'Currency Skill': 'Get currency exchange rates',
340
+ 'Asia Time Tool': 'Get current time for Asian timezones',
341
+ 'Asia Time Toolv3': 'Get current time for Asian timezones',
342
+ };
343
+ const bullets = tools.map((t) => {
344
+ const purpose = t.description || knownPurposes[t.display_name] || `Use ${t.display_name} when relevant`;
345
+ return `- **${t.display_name}**: ${purpose}`;
346
+ });
347
+ const instructions = `You are a helpful assistant with these capabilities. Use the appropriate tool when users ask:
348
+
349
+ ${bullets.join('\n')}
350
+
351
+ Be concise and accurate. Cite the tool/source when providing data.`;
352
+ await updateAgent(agent_id, { instructions });
353
+ return { success: true, agent_id, agent_name, instructions };
354
+ }
355
+ /**
356
+ * Invoke an agent by name or ID. Resolves agent_name to agent_id if needed.
357
+ * Use for "ask TimeWeatherAgent what the exchange rate is CAD/USD" – no script, runs behind the scenes.
358
+ */
359
+ export async function invokeAgentByNameOrId(args) {
360
+ const { agent_id, agent_name, message } = args;
361
+ if (!message)
362
+ throw new Error('message is required');
363
+ let agentId = agent_id;
364
+ if (!agentId && agent_name) {
365
+ const resolved = await resolveAgentByName(agent_name);
366
+ if (!resolved)
367
+ throw new Error(`Agent not found: "${agent_name}"`);
368
+ agentId = resolved;
369
+ }
370
+ if (!agentId)
371
+ throw new Error('Provide agent_id or agent_name');
372
+ return invokeAgent(agentId, message);
373
+ }
374
+ /**
375
+ * Invoke an Agent and poll for response.
376
+ */
377
+ export async function invokeAgent(agentId, message) {
378
+ console.error(`Invoking agent "${agentId}" with message: "${message}"...`);
379
+ try {
380
+ // 1. Start Run
381
+ const runPayload = {
382
+ agent_id: agentId,
383
+ message: {
384
+ role: 'user',
385
+ content: message,
386
+ },
387
+ };
388
+ const runRes = await woFetch('/v1/orchestrate/runs', {
389
+ method: 'POST',
390
+ body: JSON.stringify(runPayload),
391
+ });
392
+ if (!runRes.ok) {
393
+ const text = await runRes.text();
394
+ throw new Error(`Failed to start run: ${runRes.status} ${text}`);
395
+ }
396
+ const runData = await runRes.json();
397
+ const threadId = runData.thread_id;
398
+ const runId = runData.id;
399
+ console.error(`Status: ${runData.status} (Thread: ${threadId})`);
400
+ // 2. Poll for Completion
401
+ // We poll the messages endpoint until we see an assistant response
402
+ // that is newer than our request time (roughly)
403
+ // OR we just wait a bit and check.
404
+ // Better: Check run status first?
405
+ // Findings said: "Polling /v1/orchestrate/threads/{thread_id}/messages"
406
+ const foundResponse = false;
407
+ let pollCount = 0;
408
+ const maxPolls = 15; // 30s timeout
409
+ while (!foundResponse && pollCount < maxPolls) {
410
+ pollCount++;
411
+ await new Promise((r) => setTimeout(r, 2000)); // Wait 2s
412
+ // Check messages
413
+ const msgRes = await woFetch(`/v1/orchestrate/threads/${threadId}/messages`, { method: 'GET' });
414
+ if (msgRes.ok) {
415
+ const data = await msgRes.json();
416
+ const messages = data.data || data;
417
+ if (Array.isArray(messages)) {
418
+ // Filter for assistant messages
419
+ const assistantMsgs = messages.filter((m) => m.role === 'assistant');
420
+ if (assistantMsgs.length > 0) {
421
+ // Return the last message
422
+ const lastMsg = assistantMsgs[assistantMsgs.length - 1];
423
+ // Extract text
424
+ let responseText = 'Unknown format';
425
+ if (typeof lastMsg.content === 'string') {
426
+ responseText = lastMsg.content;
427
+ }
428
+ else if (Array.isArray(lastMsg.content)) {
429
+ responseText = lastMsg.content
430
+ .map((c) => c.text?.value || c.text || JSON.stringify(c))
431
+ .join(' ');
432
+ }
433
+ return {
434
+ success: true,
435
+ response: responseText,
436
+ thread_id: threadId,
437
+ run_id: runId,
438
+ };
439
+ }
440
+ }
441
+ }
442
+ }
443
+ throw new Error('Timed out waiting for assistant response.');
444
+ }
445
+ catch (e) {
446
+ throw new Error(`Agent Invocation Failed: ${e.message}`);
447
+ }
448
+ }
package/dist/auth.js ADDED
@@ -0,0 +1,70 @@
1
+ import { config } from './config.js';
2
+ import fetch from 'node-fetch';
3
+ let cachedToken = null;
4
+ let tokenExpiry = 0;
5
+ /**
6
+ * Get a valid IAM Bearer token.
7
+ * Refreshes if expired or missing.
8
+ */
9
+ export async function getIamToken() {
10
+ const now = Math.floor(Date.now() / 1000);
11
+ // Reuse valid token (buffer of 60s)
12
+ if (cachedToken && tokenExpiry > now + 60) {
13
+ return cachedToken;
14
+ }
15
+ console.error('Requesting new IAM token from IBM Cloud...'); // stderr for MCP logs
16
+ const params = new URLSearchParams();
17
+ params.append('grant_type', 'urn:ibm:params:oauth:grant-type:apikey');
18
+ params.append('apikey', config.apiKey);
19
+ try {
20
+ const response = await fetch('https://iam.cloud.ibm.com/identity/token', {
21
+ method: 'POST',
22
+ headers: {
23
+ 'Content-Type': 'application/x-www-form-urlencoded',
24
+ Accept: 'application/json',
25
+ },
26
+ body: params,
27
+ });
28
+ if (!response.ok) {
29
+ const text = await response.text();
30
+ throw new Error(`IAM Token Request Failed: ${response.status} ${text}`);
31
+ }
32
+ const data = (await response.json());
33
+ cachedToken = data.access_token;
34
+ tokenExpiry = now + data.expires_in;
35
+ console.error(`Token acquired (expires in ${data.expires_in}s)`);
36
+ if (!cachedToken) {
37
+ throw new Error('Failed to retrieve access token from IAM response');
38
+ }
39
+ return cachedToken;
40
+ }
41
+ catch (error) {
42
+ console.error(`Authentication Error: ${error.message}`);
43
+ throw error;
44
+ }
45
+ }
46
+ /**
47
+ * Authenticated fetch wrapper for Watson Orchestrate API.
48
+ */
49
+ export async function woFetch(endpoint, options = {}) {
50
+ const token = await getIamToken();
51
+ // Normalize endpoint (handle full URL vs relative path)
52
+ let url = endpoint;
53
+ if (!endpoint.startsWith('http')) {
54
+ // Ensure leading slash for path construction
55
+ const path = endpoint.startsWith('/') ? endpoint : `/${endpoint}`;
56
+ url = `${config.instanceUrl}${path}`;
57
+ }
58
+ const headers = {
59
+ Authorization: `Bearer ${token}`,
60
+ 'Content-Type': 'application/json',
61
+ Accept: 'application/json',
62
+ ...options.headers,
63
+ };
64
+ console.error(` MCP Fetch: ${options.method || 'GET'} ${url}`);
65
+ const response = await fetch(url, {
66
+ ...options,
67
+ headers,
68
+ });
69
+ return response;
70
+ }
package/dist/config.js ADDED
@@ -0,0 +1,39 @@
1
+ import * as dotenv from 'dotenv';
2
+ import * as path from 'path';
3
+ import { fileURLToPath } from 'url';
4
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
5
+ // Load .env from project root (assuming running from dist/ or src/)
6
+ dotenv.config({ path: path.resolve(__dirname, '..', '.env') });
7
+ // Also try cwd for good measure
8
+ dotenv.config({ path: path.join(process.cwd(), '.env') });
9
+ function parseAgentIds() {
10
+ const idsEnv = process.env.WO_AGENT_IDs ?? process.env.WO_AGENT_ID ?? '';
11
+ const agentIds = idsEnv
12
+ .split(',')
13
+ .map((s) => s.trim())
14
+ .filter(Boolean);
15
+ return {
16
+ agentIds,
17
+ agentId: agentIds[0] || '',
18
+ };
19
+ }
20
+ const { agentIds, agentId } = parseAgentIds();
21
+ export const config = {
22
+ apiKey: process.env.WO_API_KEY || '',
23
+ instanceUrl: process.env.WO_INSTANCE_URL || '',
24
+ iamTokenUrl: process.env.IAM_TOKEN_URL || 'https://iam.cloud.ibm.com/identity/token',
25
+ agentIds,
26
+ agentId,
27
+ };
28
+ export function validateConfig() {
29
+ const missing = [];
30
+ if (!config.apiKey)
31
+ missing.push('WO_API_KEY');
32
+ if (!config.instanceUrl)
33
+ missing.push('WO_INSTANCE_URL');
34
+ if (missing.length > 0) {
35
+ console.error('❌ Missing required environment variables:', missing.join(', '));
36
+ return false;
37
+ }
38
+ return true;
39
+ }