@purplesquirrel/ibmz-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,681 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * IBM Z MCP Server
5
+ *
6
+ * MCP server for IBM Z mainframe integration with Claude Code.
7
+ * Provides access to:
8
+ * - Key Protect: HSM-backed key management (FIPS 140-2 Level 3)
9
+ * - z/OS Connect: REST APIs to mainframe programs (CICS, IMS, batch)
10
+ */
11
+
12
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
13
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
14
+ import {
15
+ CallToolRequestSchema,
16
+ ListToolsRequestSchema,
17
+ } from "@modelcontextprotocol/sdk/types.js";
18
+ import IbmKeyProtectApiV2 from "@ibm-cloud/ibm-key-protect/ibm-key-protect-api/v2.js";
19
+ import { IamAuthenticator } from "ibm-cloud-sdk-core";
20
+
21
+ // Key Protect configuration
22
+ const IBM_CLOUD_API_KEY = process.env.IBM_CLOUD_API_KEY;
23
+ const KEY_PROTECT_INSTANCE_ID = process.env.KEY_PROTECT_INSTANCE_ID;
24
+ const KEY_PROTECT_URL = process.env.KEY_PROTECT_URL || "https://us-south.kms.cloud.ibm.com";
25
+
26
+ // z/OS Connect configuration (requires mainframe access)
27
+ const ZOS_CONNECT_URL = process.env.ZOS_CONNECT_URL;
28
+ const ZOS_CONNECT_USERNAME = process.env.ZOS_CONNECT_USERNAME;
29
+ const ZOS_CONNECT_PASSWORD = process.env.ZOS_CONNECT_PASSWORD;
30
+
31
+ // Initialize Key Protect client
32
+ let keyProtectClient = null;
33
+
34
+ function getKeyProtectClient() {
35
+ if (!keyProtectClient && IBM_CLOUD_API_KEY && KEY_PROTECT_INSTANCE_ID) {
36
+ keyProtectClient = new IbmKeyProtectApiV2({
37
+ authenticator: new IamAuthenticator({
38
+ apikey: IBM_CLOUD_API_KEY,
39
+ }),
40
+ serviceUrl: KEY_PROTECT_URL,
41
+ });
42
+ }
43
+ return keyProtectClient;
44
+ }
45
+
46
+ // z/OS Connect API helper
47
+ async function callZosConnect(endpoint, method = "GET", body = null) {
48
+ if (!ZOS_CONNECT_URL) {
49
+ throw new Error("z/OS Connect not configured. Set ZOS_CONNECT_URL environment variable.");
50
+ }
51
+
52
+ const url = `${ZOS_CONNECT_URL}${endpoint}`;
53
+ const headers = {
54
+ "Content-Type": "application/json",
55
+ "Accept": "application/json",
56
+ };
57
+
58
+ // Basic auth for z/OS Connect
59
+ if (ZOS_CONNECT_USERNAME && ZOS_CONNECT_PASSWORD) {
60
+ const auth = Buffer.from(`${ZOS_CONNECT_USERNAME}:${ZOS_CONNECT_PASSWORD}`).toString("base64");
61
+ headers["Authorization"] = `Basic ${auth}`;
62
+ }
63
+
64
+ const options = { method, headers };
65
+ if (body && (method === "POST" || method === "PUT" || method === "PATCH")) {
66
+ options.body = JSON.stringify(body);
67
+ }
68
+
69
+ const response = await fetch(url, options);
70
+
71
+ if (!response.ok) {
72
+ const errorText = await response.text();
73
+ throw new Error(`z/OS Connect error ${response.status}: ${errorText}`);
74
+ }
75
+
76
+ const contentType = response.headers.get("content-type");
77
+ if (contentType && contentType.includes("application/json")) {
78
+ return response.json();
79
+ }
80
+ return response.text();
81
+ }
82
+
83
+ // Create MCP server
84
+ const server = new Server(
85
+ {
86
+ name: "ibmz-mcp-server",
87
+ version: "1.0.0",
88
+ },
89
+ {
90
+ capabilities: {
91
+ tools: {},
92
+ },
93
+ }
94
+ );
95
+
96
+ // Define available tools
97
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
98
+ return {
99
+ tools: [
100
+ // ============ Key Protect Tools ============
101
+ {
102
+ name: "key_protect_list_keys",
103
+ description: "List encryption keys from IBM Key Protect (HSM-backed on IBM Z infrastructure, FIPS 140-2 Level 3 certified)",
104
+ inputSchema: {
105
+ type: "object",
106
+ properties: {
107
+ limit: {
108
+ type: "number",
109
+ description: "Maximum number of keys to return (default: 100)",
110
+ default: 100,
111
+ },
112
+ offset: {
113
+ type: "number",
114
+ description: "Number of keys to skip for pagination",
115
+ default: 0,
116
+ },
117
+ state: {
118
+ type: "array",
119
+ items: { type: "number" },
120
+ description: "Filter by key states: 1=Active, 2=Suspended, 3=Deactivated, 5=Destroyed",
121
+ },
122
+ },
123
+ },
124
+ },
125
+ {
126
+ name: "key_protect_create_key",
127
+ description: "Create a new encryption key in IBM Key Protect HSM. Root keys protect other keys (envelope encryption). Standard keys encrypt data directly.",
128
+ inputSchema: {
129
+ type: "object",
130
+ properties: {
131
+ name: {
132
+ type: "string",
133
+ description: "Unique name for the key",
134
+ },
135
+ description: {
136
+ type: "string",
137
+ description: "Description of the key's purpose",
138
+ },
139
+ type: {
140
+ type: "string",
141
+ enum: ["root_key", "standard_key"],
142
+ description: "root_key for wrapping other keys, standard_key for direct encryption",
143
+ default: "standard_key",
144
+ },
145
+ extractable: {
146
+ type: "boolean",
147
+ description: "If true, key material can be exported (only for standard_key)",
148
+ default: false,
149
+ },
150
+ payload: {
151
+ type: "string",
152
+ description: "Optional: Base64-encoded key material to import (for BYOK scenarios)",
153
+ },
154
+ },
155
+ required: ["name"],
156
+ },
157
+ },
158
+ {
159
+ name: "key_protect_get_key",
160
+ description: "Get metadata and details of a specific key from IBM Key Protect",
161
+ inputSchema: {
162
+ type: "object",
163
+ properties: {
164
+ key_id: {
165
+ type: "string",
166
+ description: "The unique ID of the key",
167
+ },
168
+ },
169
+ required: ["key_id"],
170
+ },
171
+ },
172
+ {
173
+ name: "key_protect_wrap_key",
174
+ description: "Wrap (encrypt) a data encryption key using a root key. Used for envelope encryption - protects DEKs with a KEK stored in HSM.",
175
+ inputSchema: {
176
+ type: "object",
177
+ properties: {
178
+ key_id: {
179
+ type: "string",
180
+ description: "ID of the root key to use for wrapping",
181
+ },
182
+ plaintext: {
183
+ type: "string",
184
+ description: "Base64-encoded plaintext data encryption key (DEK) to wrap",
185
+ },
186
+ aad: {
187
+ type: "array",
188
+ items: { type: "string" },
189
+ description: "Additional authenticated data for AEAD (optional, must match on unwrap)",
190
+ },
191
+ },
192
+ required: ["key_id", "plaintext"],
193
+ },
194
+ },
195
+ {
196
+ name: "key_protect_unwrap_key",
197
+ description: "Unwrap (decrypt) a wrapped data encryption key using a root key",
198
+ inputSchema: {
199
+ type: "object",
200
+ properties: {
201
+ key_id: {
202
+ type: "string",
203
+ description: "ID of the root key used to wrap the DEK",
204
+ },
205
+ ciphertext: {
206
+ type: "string",
207
+ description: "Base64-encoded wrapped (encrypted) data encryption key",
208
+ },
209
+ aad: {
210
+ type: "array",
211
+ items: { type: "string" },
212
+ description: "Additional authenticated data (must match what was used during wrap)",
213
+ },
214
+ },
215
+ required: ["key_id", "ciphertext"],
216
+ },
217
+ },
218
+ {
219
+ name: "key_protect_rotate_key",
220
+ description: "Rotate a root key. Creates new key version while maintaining ability to unwrap data encrypted with previous versions.",
221
+ inputSchema: {
222
+ type: "object",
223
+ properties: {
224
+ key_id: {
225
+ type: "string",
226
+ description: "ID of the root key to rotate",
227
+ },
228
+ payload: {
229
+ type: "string",
230
+ description: "Optional: Base64-encoded new key material for BYOK rotation",
231
+ },
232
+ },
233
+ required: ["key_id"],
234
+ },
235
+ },
236
+ {
237
+ name: "key_protect_delete_key",
238
+ description: "Delete a key from IBM Key Protect. WARNING: This is irreversible and data encrypted with this key becomes unrecoverable.",
239
+ inputSchema: {
240
+ type: "object",
241
+ properties: {
242
+ key_id: {
243
+ type: "string",
244
+ description: "ID of the key to delete",
245
+ },
246
+ force: {
247
+ type: "boolean",
248
+ description: "Force delete even if key has associated resources",
249
+ default: false,
250
+ },
251
+ },
252
+ required: ["key_id"],
253
+ },
254
+ },
255
+ {
256
+ name: "key_protect_get_key_policies",
257
+ description: "Get policies applied to a key (rotation, dual authorization)",
258
+ inputSchema: {
259
+ type: "object",
260
+ properties: {
261
+ key_id: {
262
+ type: "string",
263
+ description: "ID of the key",
264
+ },
265
+ },
266
+ required: ["key_id"],
267
+ },
268
+ },
269
+
270
+ // ============ z/OS Connect Tools ============
271
+ {
272
+ name: "zos_connect_list_services",
273
+ description: "List all available z/OS Connect services (REST APIs exposing mainframe programs). Each service maps to CICS transactions, IMS programs, or batch jobs.",
274
+ inputSchema: {
275
+ type: "object",
276
+ properties: {},
277
+ },
278
+ },
279
+ {
280
+ name: "zos_connect_get_service",
281
+ description: "Get detailed information about a z/OS Connect service including its OpenAPI specification and endpoint mappings",
282
+ inputSchema: {
283
+ type: "object",
284
+ properties: {
285
+ service_name: {
286
+ type: "string",
287
+ description: "Name of the z/OS Connect service",
288
+ },
289
+ },
290
+ required: ["service_name"],
291
+ },
292
+ },
293
+ {
294
+ name: "zos_connect_call_service",
295
+ description: "Call a z/OS Connect REST API to interact with mainframe programs. Maps JSON to COBOL copybooks automatically.",
296
+ inputSchema: {
297
+ type: "object",
298
+ properties: {
299
+ service_name: {
300
+ type: "string",
301
+ description: "Name of the z/OS Connect service to invoke",
302
+ },
303
+ operation: {
304
+ type: "string",
305
+ description: "API operation path (e.g., /accounts, /customers/{id})",
306
+ default: "/",
307
+ },
308
+ method: {
309
+ type: "string",
310
+ enum: ["GET", "POST", "PUT", "DELETE", "PATCH"],
311
+ description: "HTTP method",
312
+ default: "POST",
313
+ },
314
+ payload: {
315
+ type: "object",
316
+ description: "JSON request body (automatically mapped to COBOL structures)",
317
+ },
318
+ path_params: {
319
+ type: "object",
320
+ description: "Path parameters for URL template substitution",
321
+ },
322
+ query_params: {
323
+ type: "object",
324
+ description: "Query string parameters",
325
+ },
326
+ },
327
+ required: ["service_name"],
328
+ },
329
+ },
330
+ {
331
+ name: "zos_connect_list_apis",
332
+ description: "List all deployed API requester configurations (outbound calls from z/OS to external services)",
333
+ inputSchema: {
334
+ type: "object",
335
+ properties: {},
336
+ },
337
+ },
338
+ {
339
+ name: "zos_connect_health",
340
+ description: "Check the health status of the z/OS Connect server and connected subsystems",
341
+ inputSchema: {
342
+ type: "object",
343
+ properties: {},
344
+ },
345
+ },
346
+ ],
347
+ };
348
+ });
349
+
350
+ // Handle tool calls
351
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
352
+ const { name, arguments: args } = request.params;
353
+
354
+ try {
355
+ // ============ Key Protect Tool Handlers ============
356
+ if (name.startsWith("key_protect_")) {
357
+ const kpClient = getKeyProtectClient();
358
+
359
+ if (!kpClient) {
360
+ return {
361
+ content: [{
362
+ type: "text",
363
+ text: "Error: Key Protect not configured. Set IBM_CLOUD_API_KEY and KEY_PROTECT_INSTANCE_ID environment variables.",
364
+ }],
365
+ };
366
+ }
367
+
368
+ switch (name) {
369
+ case "key_protect_list_keys": {
370
+ const response = await kpClient.getKeys({
371
+ bluemixInstance: KEY_PROTECT_INSTANCE_ID,
372
+ limit: args.limit || 100,
373
+ offset: args.offset || 0,
374
+ state: args.state,
375
+ });
376
+
377
+ const keys = response.result.resources?.map(k => ({
378
+ id: k.id,
379
+ name: k.name,
380
+ type: k.type,
381
+ state: k.state,
382
+ extractable: k.extractable,
383
+ crn: k.crn,
384
+ createdBy: k.createdBy,
385
+ creationDate: k.creationDate,
386
+ lastRotateDate: k.lastRotateDate,
387
+ deleted: k.deleted,
388
+ })) || [];
389
+
390
+ return {
391
+ content: [{
392
+ type: "text",
393
+ text: JSON.stringify({ total: keys.length, keys }, null, 2),
394
+ }],
395
+ };
396
+ }
397
+
398
+ case "key_protect_create_key": {
399
+ const isRootKey = args.type === "root_key";
400
+
401
+ const keyResource = {
402
+ type: "application/vnd.ibm.kms.key+json",
403
+ name: args.name,
404
+ extractable: isRootKey ? false : (args.extractable || false),
405
+ };
406
+
407
+ if (args.description) {
408
+ keyResource.description = args.description;
409
+ }
410
+
411
+ if (args.payload) {
412
+ keyResource.payload = args.payload;
413
+ }
414
+
415
+ const keyCreateBody = {
416
+ metadata: {
417
+ collectionType: "application/vnd.ibm.kms.key+json",
418
+ collectionTotal: 1,
419
+ },
420
+ resources: [keyResource],
421
+ };
422
+
423
+ const response = await kpClient.createKey({
424
+ bluemixInstance: KEY_PROTECT_INSTANCE_ID,
425
+ keyCreateBody,
426
+ });
427
+
428
+ return {
429
+ content: [{
430
+ type: "text",
431
+ text: JSON.stringify({
432
+ message: `Key "${args.name}" created successfully`,
433
+ key: {
434
+ id: response.result.resources?.[0]?.id,
435
+ name: response.result.resources?.[0]?.name,
436
+ type: response.result.resources?.[0]?.type,
437
+ crn: response.result.resources?.[0]?.crn,
438
+ },
439
+ }, null, 2),
440
+ }],
441
+ };
442
+ }
443
+
444
+ case "key_protect_get_key": {
445
+ const response = await kpClient.getKey({
446
+ bluemixInstance: KEY_PROTECT_INSTANCE_ID,
447
+ id: args.key_id,
448
+ });
449
+
450
+ const key = response.result.resources?.[0];
451
+ return {
452
+ content: [{
453
+ type: "text",
454
+ text: JSON.stringify(key, null, 2),
455
+ }],
456
+ };
457
+ }
458
+
459
+ case "key_protect_wrap_key": {
460
+ const keyActionWrapBody = {
461
+ plaintext: args.plaintext,
462
+ };
463
+
464
+ if (args.aad) {
465
+ keyActionWrapBody.aad = args.aad;
466
+ }
467
+
468
+ const response = await kpClient.wrapKey({
469
+ id: args.key_id,
470
+ bluemixInstance: KEY_PROTECT_INSTANCE_ID,
471
+ keyActionWrapBody,
472
+ });
473
+
474
+ return {
475
+ content: [{
476
+ type: "text",
477
+ text: JSON.stringify({
478
+ message: "Key wrapped successfully",
479
+ ciphertext: response.result.ciphertext,
480
+ keyVersion: response.result.keyVersion,
481
+ }, null, 2),
482
+ }],
483
+ };
484
+ }
485
+
486
+ case "key_protect_unwrap_key": {
487
+ const keyActionUnwrapBody = {
488
+ ciphertext: args.ciphertext,
489
+ };
490
+
491
+ if (args.aad) {
492
+ keyActionUnwrapBody.aad = args.aad;
493
+ }
494
+
495
+ const response = await kpClient.unwrapKey({
496
+ id: args.key_id,
497
+ bluemixInstance: KEY_PROTECT_INSTANCE_ID,
498
+ keyActionUnwrapBody,
499
+ });
500
+
501
+ return {
502
+ content: [{
503
+ type: "text",
504
+ text: JSON.stringify({
505
+ message: "Key unwrapped successfully",
506
+ plaintext: response.result.plaintext,
507
+ keyVersion: response.result.keyVersion,
508
+ }, null, 2),
509
+ }],
510
+ };
511
+ }
512
+
513
+ case "key_protect_rotate_key": {
514
+ const params = {
515
+ id: args.key_id,
516
+ bluemixInstance: KEY_PROTECT_INSTANCE_ID,
517
+ };
518
+
519
+ if (args.payload) {
520
+ params.keyActionRotateBody = {
521
+ payload: args.payload,
522
+ };
523
+ }
524
+
525
+ await kpClient.rotateKey(params);
526
+
527
+ return {
528
+ content: [{
529
+ type: "text",
530
+ text: JSON.stringify({
531
+ message: `Key ${args.key_id} rotated successfully`,
532
+ keyId: args.key_id,
533
+ }, null, 2),
534
+ }],
535
+ };
536
+ }
537
+
538
+ case "key_protect_delete_key": {
539
+ await kpClient.deleteKey({
540
+ bluemixInstance: KEY_PROTECT_INSTANCE_ID,
541
+ id: args.key_id,
542
+ force: args.force || false,
543
+ });
544
+
545
+ return {
546
+ content: [{
547
+ type: "text",
548
+ text: JSON.stringify({
549
+ message: `Key ${args.key_id} deleted successfully`,
550
+ warning: "This action is irreversible. Data encrypted with this key is now unrecoverable.",
551
+ }, null, 2),
552
+ }],
553
+ };
554
+ }
555
+
556
+ case "key_protect_get_key_policies": {
557
+ const response = await kpClient.getKeyMetadata({
558
+ bluemixInstance: KEY_PROTECT_INSTANCE_ID,
559
+ id: args.key_id,
560
+ });
561
+
562
+ return {
563
+ content: [{
564
+ type: "text",
565
+ text: JSON.stringify(response.result, null, 2),
566
+ }],
567
+ };
568
+ }
569
+ }
570
+ }
571
+
572
+ // ============ z/OS Connect Tool Handlers ============
573
+ if (name.startsWith("zos_connect_")) {
574
+ if (!ZOS_CONNECT_URL) {
575
+ return {
576
+ content: [{
577
+ type: "text",
578
+ text: "Error: z/OS Connect not configured. Set ZOS_CONNECT_URL environment variable. This requires access to an IBM mainframe with z/OS Connect EE installed.",
579
+ }],
580
+ };
581
+ }
582
+
583
+ switch (name) {
584
+ case "zos_connect_list_services": {
585
+ const services = await callZosConnect("/zosConnect/services");
586
+ return {
587
+ content: [{
588
+ type: "text",
589
+ text: JSON.stringify(services, null, 2),
590
+ }],
591
+ };
592
+ }
593
+
594
+ case "zos_connect_get_service": {
595
+ const service = await callZosConnect(`/zosConnect/services/${args.service_name}`);
596
+ return {
597
+ content: [{
598
+ type: "text",
599
+ text: JSON.stringify(service, null, 2),
600
+ }],
601
+ };
602
+ }
603
+
604
+ case "zos_connect_call_service": {
605
+ let endpoint = `/zosConnect/services/${args.service_name}${args.operation || "/"}`;
606
+
607
+ // Substitute path parameters
608
+ if (args.path_params) {
609
+ for (const [key, value] of Object.entries(args.path_params)) {
610
+ endpoint = endpoint.replace(`{${key}}`, encodeURIComponent(String(value)));
611
+ }
612
+ }
613
+
614
+ // Add query parameters
615
+ if (args.query_params) {
616
+ const queryString = new URLSearchParams(args.query_params).toString();
617
+ endpoint += `?${queryString}`;
618
+ }
619
+
620
+ const result = await callZosConnect(endpoint, args.method || "POST", args.payload);
621
+
622
+ return {
623
+ content: [{
624
+ type: "text",
625
+ text: JSON.stringify({
626
+ service: args.service_name,
627
+ operation: args.operation || "/",
628
+ method: args.method || "POST",
629
+ response: result,
630
+ }, null, 2),
631
+ }],
632
+ };
633
+ }
634
+
635
+ case "zos_connect_list_apis": {
636
+ const apis = await callZosConnect("/zosConnect/apis");
637
+ return {
638
+ content: [{
639
+ type: "text",
640
+ text: JSON.stringify(apis, null, 2),
641
+ }],
642
+ };
643
+ }
644
+
645
+ case "zos_connect_health": {
646
+ const health = await callZosConnect("/zosConnect/health");
647
+ return {
648
+ content: [{
649
+ type: "text",
650
+ text: JSON.stringify(health, null, 2),
651
+ }],
652
+ };
653
+ }
654
+ }
655
+ }
656
+
657
+ return {
658
+ content: [{
659
+ type: "text",
660
+ text: `Unknown tool: ${name}`,
661
+ }],
662
+ };
663
+
664
+ } catch (error) {
665
+ return {
666
+ content: [{
667
+ type: "text",
668
+ text: `Error: ${error.message}`,
669
+ }],
670
+ };
671
+ }
672
+ });
673
+
674
+ // Start server
675
+ async function main() {
676
+ const transport = new StdioServerTransport();
677
+ await server.connect(transport);
678
+ console.error("IBM Z MCP server running on stdio");
679
+ }
680
+
681
+ main().catch(console.error);