@purplesquirrel/watsonx-mcp-server 1.0.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/index.js ADDED
@@ -0,0 +1,551 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
4
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
+ import {
6
+ CallToolRequestSchema,
7
+ ListToolsRequestSchema,
8
+ } from "@modelcontextprotocol/sdk/types.js";
9
+ import { WatsonXAI } from "@ibm-cloud/watsonx-ai";
10
+ import { IamAuthenticator } from "ibm-cloud-sdk-core";
11
+ // import KeyProtectV2 from "@ibm-cloud/key-protect"; // Temporarily disabled - package not available
12
+
13
+ // Configuration from environment
14
+ const WATSONX_API_KEY = process.env.WATSONX_API_KEY;
15
+ const WATSONX_PROJECT_ID = process.env.WATSONX_PROJECT_ID;
16
+ const WATSONX_SPACE_ID = process.env.WATSONX_SPACE_ID; // Deployment space (preferred)
17
+ const WATSONX_URL = process.env.WATSONX_URL || "https://us-south.ml.cloud.ibm.com";
18
+
19
+ // IBM Z / Key Protect configuration
20
+ const KEY_PROTECT_API_KEY = process.env.KEY_PROTECT_API_KEY || process.env.WATSONX_API_KEY;
21
+ const KEY_PROTECT_INSTANCE_ID = process.env.KEY_PROTECT_INSTANCE_ID;
22
+ const KEY_PROTECT_URL = process.env.KEY_PROTECT_URL || "https://us-south.kms.cloud.ibm.com";
23
+
24
+ // z/OS Connect configuration (optional - requires mainframe access)
25
+ const ZOS_CONNECT_URL = process.env.ZOS_CONNECT_URL;
26
+ const ZOS_CONNECT_API_KEY = process.env.ZOS_CONNECT_API_KEY;
27
+
28
+ // Initialize watsonx.ai client
29
+ let watsonxClient = null;
30
+ let keyProtectClient = null;
31
+
32
+ function getWatsonxClient() {
33
+ if (!watsonxClient && WATSONX_API_KEY) {
34
+ watsonxClient = WatsonXAI.newInstance({
35
+ version: "2024-05-31",
36
+ serviceUrl: WATSONX_URL,
37
+ authenticator: new IamAuthenticator({
38
+ apikey: WATSONX_API_KEY,
39
+ }),
40
+ });
41
+ }
42
+ return watsonxClient;
43
+ }
44
+
45
+ // Initialize Key Protect client (IBM Z HSM-backed key management)
46
+ function getKeyProtectClient() {
47
+ // Temporarily disabled - @ibm-cloud/key-protect package not available
48
+ // if (!keyProtectClient && KEY_PROTECT_API_KEY && KEY_PROTECT_INSTANCE_ID) {
49
+ // keyProtectClient = KeyProtectV2.newInstance({
50
+ // authenticator: new IamAuthenticator({
51
+ // apikey: KEY_PROTECT_API_KEY,
52
+ // }),
53
+ // serviceUrl: KEY_PROTECT_URL,
54
+ // });
55
+ // keyProtectClient.setServiceUrl(KEY_PROTECT_URL);
56
+ // }
57
+ return null; // Disabled
58
+ }
59
+
60
+ // z/OS Connect API caller (for mainframe integration)
61
+ async function callZosConnect(endpoint, method = "GET", body = null) {
62
+ if (!ZOS_CONNECT_URL) {
63
+ throw new Error("z/OS Connect not configured. Set ZOS_CONNECT_URL environment variable.");
64
+ }
65
+
66
+ const url = `${ZOS_CONNECT_URL}${endpoint}`;
67
+ const headers = {
68
+ "Content-Type": "application/json",
69
+ "Accept": "application/json",
70
+ };
71
+
72
+ if (ZOS_CONNECT_API_KEY) {
73
+ headers["Authorization"] = `Bearer ${ZOS_CONNECT_API_KEY}`;
74
+ }
75
+
76
+ const options = { method, headers };
77
+ if (body && (method === "POST" || method === "PUT")) {
78
+ options.body = JSON.stringify(body);
79
+ }
80
+
81
+ const response = await fetch(url, options);
82
+ if (!response.ok) {
83
+ throw new Error(`z/OS Connect error: ${response.status} ${response.statusText}`);
84
+ }
85
+ return response.json();
86
+ }
87
+
88
+ // Create MCP server
89
+ const server = new Server(
90
+ {
91
+ name: "watsonx-ibmz-mcp-server",
92
+ version: "2.0.0",
93
+ },
94
+ {
95
+ capabilities: {
96
+ tools: {},
97
+ },
98
+ }
99
+ );
100
+
101
+ // Define available tools
102
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
103
+ return {
104
+ tools: [
105
+ {
106
+ name: "watsonx_generate",
107
+ description: "Generate text using IBM watsonx.ai foundation models (Granite, Llama, Mistral, etc.)",
108
+ inputSchema: {
109
+ type: "object",
110
+ properties: {
111
+ prompt: {
112
+ type: "string",
113
+ description: "The prompt to send to the model",
114
+ },
115
+ model_id: {
116
+ type: "string",
117
+ description: "Model ID (e.g., 'ibm/granite-3-3-8b-instruct', 'meta-llama/llama-3-70b-instruct')",
118
+ default: "ibm/granite-3-3-8b-instruct",
119
+ },
120
+ max_new_tokens: {
121
+ type: "number",
122
+ description: "Maximum number of tokens to generate",
123
+ default: 500,
124
+ },
125
+ temperature: {
126
+ type: "number",
127
+ description: "Temperature for sampling (0-2)",
128
+ default: 0.7,
129
+ },
130
+ top_p: {
131
+ type: "number",
132
+ description: "Top-p nucleus sampling",
133
+ default: 1.0,
134
+ },
135
+ top_k: {
136
+ type: "number",
137
+ description: "Top-k sampling",
138
+ default: 50,
139
+ },
140
+ },
141
+ required: ["prompt"],
142
+ },
143
+ },
144
+ {
145
+ name: "watsonx_list_models",
146
+ description: "List available foundation models in watsonx.ai",
147
+ inputSchema: {
148
+ type: "object",
149
+ properties: {},
150
+ },
151
+ },
152
+ {
153
+ name: "watsonx_embeddings",
154
+ description: "Generate text embeddings using watsonx.ai embedding models",
155
+ inputSchema: {
156
+ type: "object",
157
+ properties: {
158
+ texts: {
159
+ type: "array",
160
+ items: { type: "string" },
161
+ description: "Array of texts to embed",
162
+ },
163
+ model_id: {
164
+ type: "string",
165
+ description: "Embedding model ID",
166
+ default: "ibm/slate-125m-english-rtrvr-v2",
167
+ },
168
+ },
169
+ required: ["texts"],
170
+ },
171
+ },
172
+ {
173
+ name: "watsonx_chat",
174
+ description: "Have a conversation with watsonx.ai chat models",
175
+ inputSchema: {
176
+ type: "object",
177
+ properties: {
178
+ messages: {
179
+ type: "array",
180
+ items: {
181
+ type: "object",
182
+ properties: {
183
+ role: { type: "string", enum: ["system", "user", "assistant"] },
184
+ content: { type: "string" },
185
+ },
186
+ },
187
+ description: "Array of chat messages",
188
+ },
189
+ model_id: {
190
+ type: "string",
191
+ description: "Chat model ID",
192
+ default: "ibm/granite-3-3-8b-instruct",
193
+ },
194
+ max_new_tokens: {
195
+ type: "number",
196
+ default: 500,
197
+ },
198
+ temperature: {
199
+ type: "number",
200
+ default: 0.7,
201
+ },
202
+ },
203
+ required: ["messages"],
204
+ },
205
+ },
206
+ // IBM Z / Key Protect Tools
207
+ {
208
+ name: "key_protect_list_keys",
209
+ description: "List encryption keys from IBM Key Protect (HSM-backed key management on IBM Z infrastructure)",
210
+ inputSchema: {
211
+ type: "object",
212
+ properties: {
213
+ limit: {
214
+ type: "number",
215
+ description: "Maximum number of keys to return",
216
+ default: 100,
217
+ },
218
+ offset: {
219
+ type: "number",
220
+ description: "Offset for pagination",
221
+ default: 0,
222
+ },
223
+ },
224
+ },
225
+ },
226
+ {
227
+ name: "key_protect_create_key",
228
+ description: "Create a new encryption key in IBM Key Protect (stored in FIPS 140-2 Level 3 HSM)",
229
+ inputSchema: {
230
+ type: "object",
231
+ properties: {
232
+ name: {
233
+ type: "string",
234
+ description: "Name for the new key",
235
+ },
236
+ description: {
237
+ type: "string",
238
+ description: "Description of the key's purpose",
239
+ },
240
+ type: {
241
+ type: "string",
242
+ enum: ["root_key", "standard_key"],
243
+ description: "Key type: root_key (for wrapping) or standard_key (for encryption)",
244
+ default: "standard_key",
245
+ },
246
+ extractable: {
247
+ type: "boolean",
248
+ description: "Whether the key material can be extracted",
249
+ default: false,
250
+ },
251
+ },
252
+ required: ["name"],
253
+ },
254
+ },
255
+ {
256
+ name: "key_protect_get_key",
257
+ description: "Get details of a specific key from IBM Key Protect",
258
+ inputSchema: {
259
+ type: "object",
260
+ properties: {
261
+ key_id: {
262
+ type: "string",
263
+ description: "The ID of the key to retrieve",
264
+ },
265
+ },
266
+ required: ["key_id"],
267
+ },
268
+ },
269
+ {
270
+ name: "key_protect_wrap_key",
271
+ description: "Wrap (encrypt) data using a root key in IBM Key Protect - for envelope encryption",
272
+ inputSchema: {
273
+ type: "object",
274
+ properties: {
275
+ key_id: {
276
+ type: "string",
277
+ description: "The ID of the root key to use for wrapping",
278
+ },
279
+ plaintext: {
280
+ type: "string",
281
+ description: "Base64-encoded data encryption key to wrap",
282
+ },
283
+ aad: {
284
+ type: "array",
285
+ items: { type: "string" },
286
+ description: "Additional authentication data (AAD) for AEAD encryption",
287
+ },
288
+ },
289
+ required: ["key_id", "plaintext"],
290
+ },
291
+ },
292
+ {
293
+ name: "key_protect_unwrap_key",
294
+ description: "Unwrap (decrypt) data using a root key in IBM Key Protect",
295
+ inputSchema: {
296
+ type: "object",
297
+ properties: {
298
+ key_id: {
299
+ type: "string",
300
+ description: "The ID of the root key to use for unwrapping",
301
+ },
302
+ ciphertext: {
303
+ type: "string",
304
+ description: "Base64-encoded wrapped data encryption key",
305
+ },
306
+ aad: {
307
+ type: "array",
308
+ items: { type: "string" },
309
+ description: "Additional authentication data (must match wrap AAD)",
310
+ },
311
+ },
312
+ required: ["key_id", "ciphertext"],
313
+ },
314
+ },
315
+ {
316
+ name: "key_protect_delete_key",
317
+ description: "Delete an encryption key from IBM Key Protect (irreversible)",
318
+ inputSchema: {
319
+ type: "object",
320
+ properties: {
321
+ key_id: {
322
+ type: "string",
323
+ description: "The ID of the key to delete",
324
+ },
325
+ force: {
326
+ type: "boolean",
327
+ description: "Force deletion even if key has associated resources",
328
+ default: false,
329
+ },
330
+ },
331
+ required: ["key_id"],
332
+ },
333
+ },
334
+ // z/OS Connect Tools (requires mainframe access)
335
+ {
336
+ name: "zos_connect_list_services",
337
+ description: "List available z/OS Connect services (RESTful APIs to mainframe programs). Requires ZOS_CONNECT_URL to be configured.",
338
+ inputSchema: {
339
+ type: "object",
340
+ properties: {},
341
+ },
342
+ },
343
+ {
344
+ name: "zos_connect_call_service",
345
+ description: "Call a z/OS Connect service to interact with mainframe programs (CICS, IMS, batch). Requires ZOS_CONNECT_URL to be configured.",
346
+ inputSchema: {
347
+ type: "object",
348
+ properties: {
349
+ service_name: {
350
+ type: "string",
351
+ description: "Name of the z/OS Connect service to call",
352
+ },
353
+ operation: {
354
+ type: "string",
355
+ description: "Operation/method to invoke (e.g., GET, POST)",
356
+ default: "POST",
357
+ },
358
+ payload: {
359
+ type: "object",
360
+ description: "JSON payload to send to the mainframe service",
361
+ },
362
+ },
363
+ required: ["service_name"],
364
+ },
365
+ },
366
+ {
367
+ name: "zos_connect_get_service_info",
368
+ description: "Get detailed information about a z/OS Connect service including its OpenAPI specification",
369
+ inputSchema: {
370
+ type: "object",
371
+ properties: {
372
+ service_name: {
373
+ type: "string",
374
+ description: "Name of the z/OS Connect service",
375
+ },
376
+ },
377
+ required: ["service_name"],
378
+ },
379
+ },
380
+ ],
381
+ };
382
+ });
383
+
384
+ // Handle tool calls
385
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
386
+ const { name, arguments: args } = request.params;
387
+ const client = getWatsonxClient();
388
+
389
+ if (!client) {
390
+ return {
391
+ content: [
392
+ {
393
+ type: "text",
394
+ text: "Error: watsonx.ai not configured. Set WATSONX_API_KEY environment variable.",
395
+ },
396
+ ],
397
+ };
398
+ }
399
+
400
+ try {
401
+ switch (name) {
402
+ case "watsonx_generate": {
403
+ const params = {
404
+ input: args.prompt,
405
+ modelId: args.model_id || "ibm/granite-3-3-8b-instruct",
406
+ parameters: {
407
+ max_new_tokens: args.max_new_tokens || 500,
408
+ temperature: args.temperature || 0.7,
409
+ top_p: args.top_p || 1.0,
410
+ top_k: args.top_k || 50,
411
+ },
412
+ };
413
+
414
+ // Add spaceId (preferred) or projectId
415
+ if (WATSONX_SPACE_ID) {
416
+ params.spaceId = WATSONX_SPACE_ID;
417
+ } else if (WATSONX_PROJECT_ID) {
418
+ params.projectId = WATSONX_PROJECT_ID;
419
+ }
420
+
421
+ const response = await client.generateText(params);
422
+
423
+ const generatedText = response.result.results?.[0]?.generated_text || "";
424
+ return {
425
+ content: [
426
+ {
427
+ type: "text",
428
+ text: generatedText,
429
+ },
430
+ ],
431
+ };
432
+ }
433
+
434
+ case "watsonx_list_models": {
435
+ const response = await client.listFoundationModelSpecs({
436
+ limit: 100,
437
+ });
438
+
439
+ const models = response.result.resources?.map((m) => ({
440
+ id: m.model_id,
441
+ name: m.label,
442
+ provider: m.provider,
443
+ tasks: m.tasks,
444
+ })) || [];
445
+
446
+ return {
447
+ content: [
448
+ {
449
+ type: "text",
450
+ text: JSON.stringify(models, null, 2),
451
+ },
452
+ ],
453
+ };
454
+ }
455
+
456
+ case "watsonx_embeddings": {
457
+ const params = {
458
+ inputs: args.texts,
459
+ modelId: args.model_id || "ibm/slate-125m-english-rtrvr-v2",
460
+ };
461
+
462
+ // Add spaceId (preferred) or projectId
463
+ if (WATSONX_SPACE_ID) {
464
+ params.spaceId = WATSONX_SPACE_ID;
465
+ } else if (WATSONX_PROJECT_ID) {
466
+ params.projectId = WATSONX_PROJECT_ID;
467
+ }
468
+
469
+ const response = await client.embedText(params);
470
+
471
+ return {
472
+ content: [
473
+ {
474
+ type: "text",
475
+ text: JSON.stringify(response.result, null, 2),
476
+ },
477
+ ],
478
+ };
479
+ }
480
+
481
+ case "watsonx_chat": {
482
+ // Format messages for chat completion
483
+ const formattedPrompt = args.messages
484
+ .map((m) => {
485
+ if (m.role === "system") return `System: ${m.content}`;
486
+ if (m.role === "user") return `User: ${m.content}`;
487
+ if (m.role === "assistant") return `Assistant: ${m.content}`;
488
+ return m.content;
489
+ })
490
+ .join("\n\n");
491
+
492
+ const params = {
493
+ input: formattedPrompt + "\n\nAssistant:",
494
+ modelId: args.model_id || "ibm/granite-3-3-8b-instruct",
495
+ parameters: {
496
+ max_new_tokens: args.max_new_tokens || 500,
497
+ temperature: args.temperature || 0.7,
498
+ stop_sequences: ["User:", "System:"],
499
+ },
500
+ };
501
+
502
+ // Add spaceId (preferred) or projectId
503
+ if (WATSONX_SPACE_ID) {
504
+ params.spaceId = WATSONX_SPACE_ID;
505
+ } else if (WATSONX_PROJECT_ID) {
506
+ params.projectId = WATSONX_PROJECT_ID;
507
+ }
508
+
509
+ const response = await client.generateText(params);
510
+
511
+ const generatedText = response.result.results?.[0]?.generated_text || "";
512
+ return {
513
+ content: [
514
+ {
515
+ type: "text",
516
+ text: generatedText.trim(),
517
+ },
518
+ ],
519
+ };
520
+ }
521
+
522
+ default:
523
+ return {
524
+ content: [
525
+ {
526
+ type: "text",
527
+ text: `Unknown tool: ${name}`,
528
+ },
529
+ ],
530
+ };
531
+ }
532
+ } catch (error) {
533
+ return {
534
+ content: [
535
+ {
536
+ type: "text",
537
+ text: `Error calling watsonx.ai: ${error.message}`,
538
+ },
539
+ ],
540
+ };
541
+ }
542
+ });
543
+
544
+ // Start server
545
+ async function main() {
546
+ const transport = new StdioServerTransport();
547
+ await server.connect(transport);
548
+ console.error("watsonx MCP server running on stdio");
549
+ }
550
+
551
+ main().catch(console.error);
@@ -0,0 +1,92 @@
1
+ # LinkedIn Post - watsonx MCP Server
2
+
3
+ ---
4
+
5
+ ## Post Option 1 (Technical Focus)
6
+
7
+ **Just shipped: watsonx MCP Server** 🚀
8
+
9
+ I built a Model Context Protocol server that lets Claude delegate tasks to IBM watsonx.ai foundation models.
10
+
11
+ **The result?** A two-agent AI system where Claude (Opus 4.5) handles complex reasoning while delegating specific workloads to Granite, Llama 3, and Mistral models.
12
+
13
+ **What it does:**
14
+ • `watsonx_generate` - Text generation with enterprise models
15
+ • `watsonx_chat` - Multi-turn conversations
16
+ • `watsonx_embeddings` - Vector embeddings for RAG pipelines
17
+ • `watsonx_list_models` - Model discovery
18
+
19
+ **Why it matters:**
20
+ → Cost optimization by routing to smaller models
21
+ → Enterprise compliance with IBM infrastructure
22
+ → Specialized model capabilities (Granite for enterprise, Llama for reasoning)
23
+ → Embedding generation for semantic search
24
+
25
+ Built with Node.js, the @IBM watsonx-ai SDK, and the MCP protocol.
26
+
27
+ 📖 Live Demo: https://purplesquirrelmedia.github.io/watsonx-mcp-server/
28
+ 💻 Source: https://github.com/PurpleSquirrelMedia/watsonx-mcp-server
29
+
30
+ This is what multi-agent AI looks like in practice—not replacing models, but orchestrating them.
31
+
32
+ #AI #watsonx #IBM #Claude #MCP #MachineLearning #Anthropic #LLM #OpenSource
33
+
34
+ ---
35
+
36
+ ## Post Option 2 (Narrative Focus)
37
+
38
+ **What if Claude could talk to other AI models?**
39
+
40
+ That's exactly what I built.
41
+
42
+ The watsonx MCP Server creates a bridge between Claude and IBM's foundation models. Now when I'm working in Claude Code, it can:
43
+
44
+ ✅ Delegate text generation to IBM Granite models
45
+ ✅ Generate embeddings with Slate for RAG pipelines
46
+ ✅ Use Llama 3 70B for specific reasoning tasks
47
+ ✅ Route to cost-effective models for simple tasks
48
+
49
+ **The architecture is simple:**
50
+ Claude (primary agent) → MCP Server → watsonx.ai API → Foundation Models
51
+
52
+ **The implications are profound:**
53
+ This isn't just API integration. It's the beginning of AI systems that can intelligently select which model to use for which task.
54
+
55
+ Claude handles the orchestration. Watson handles the specialized workloads.
56
+
57
+ **Try it yourself:**
58
+ 📖 https://purplesquirrelmedia.github.io/watsonx-mcp-server/
59
+ 💻 https://github.com/PurpleSquirrelMedia/watsonx-mcp-server
60
+
61
+ MIT licensed. Built in an afternoon with Claude Code.
62
+
63
+ The future of AI isn't one model to rule them all—it's intelligent orchestration.
64
+
65
+ #AI #TwoAgentArchitecture #watsonx #Claude #IBM #Anthropic #MCP
66
+
67
+ ---
68
+
69
+ ## Post Option 3 (Short & Punchy)
70
+
71
+ **New drop: watsonx MCP Server** âš¡
72
+
73
+ Claude can now delegate tasks to IBM watsonx.ai.
74
+
75
+ Two agents. One workflow. Infinite possibilities.
76
+
77
+ → Text generation with Granite & Llama
78
+ → Embeddings for RAG
79
+ → Model-specific routing
80
+ → Enterprise compliance
81
+
82
+ Demo: https://purplesquirrelmedia.github.io/watsonx-mcp-server/
83
+ Code: https://github.com/PurpleSquirrelMedia/watsonx-mcp-server
84
+
85
+ This is the future of AI development—not choosing between models, but orchestrating them.
86
+
87
+ #watsonx #Claude #MCP #AI #IBM #Anthropic
88
+
89
+ ---
90
+
91
+ ## Suggested Hashtags (pick 5-7):
92
+ #AI #ArtificialIntelligence #watsonx #IBM #Claude #Anthropic #MCP #ModelContextProtocol #LLM #MachineLearning #OpenSource #TwoAgentAI #RAG #Embeddings #FoundationModels #NodeJS #Developer #TechInnovation
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "@purplesquirrel/watsonx-mcp-server",
3
+ "version": "1.0.0",
4
+ "description": "MCP server for IBM watsonx.ai integration with Claude",
5
+ "main": "index.js",
6
+ "type": "module",
7
+ "bin": {
8
+ "watsonx-mcp-server": "./index.js"
9
+ },
10
+ "scripts": {
11
+ "start": "node index.js",
12
+ "test": "echo \"Error: no test specified\" && exit 1"
13
+ },
14
+ "keywords": [
15
+ "mcp",
16
+ "watsonx",
17
+ "ibm",
18
+ "claude",
19
+ "ai"
20
+ ],
21
+ "author": "Matthew Karsten",
22
+ "license": "MIT",
23
+ "dependencies": {
24
+ "@ibm-cloud/watsonx-ai": "^1.7.5",
25
+ "@modelcontextprotocol/sdk": "^1.24.3",
26
+ "ibm-cloud-sdk-core": "^5.4.5"
27
+ }
28
+ }
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env node
2
+ import { WatsonXAI } from '@ibm-cloud/watsonx-ai';
3
+ import { IamAuthenticator } from 'ibm-cloud-sdk-core';
4
+
5
+ const client = WatsonXAI.newInstance({
6
+ version: '2024-05-31',
7
+ serviceUrl: process.env.WATSONX_URL || 'https://us-south.ml.cloud.ibm.com',
8
+ authenticator: new IamAuthenticator({
9
+ apikey: process.env.WATSONX_API_KEY,
10
+ }),
11
+ });
12
+
13
+ console.log('=== watsonx.ai MCP Server Test ===\n');
14
+
15
+ // List available models
16
+ console.log('1. Listing foundation models...');
17
+ const modelsResp = await client.listFoundationModelSpecs({ limit: 10 });
18
+ const models = modelsResp.result.resources || [];
19
+ console.log(` Found ${models.length} models:`);
20
+ models.slice(0, 5).forEach(m => {
21
+ console.log(` - ${m.model_id}`);
22
+ });
23
+ if (models.length > 5) {
24
+ console.log(` ... and ${models.length - 5} more`);
25
+ }
26
+
27
+ console.log('\n✅ watsonx.ai connection successful!');
28
+ console.log('\nNote: Text generation requires a Project ID. Create a project at:');
29
+ console.log('https://dataplatform.cloud.ibm.com');