From be98425fc7ad4abf567c7244c3f3f034cef35686 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 13 Apr 2026 19:24:33 +0000 Subject: [PATCH 1/9] Initial plan From 762e99382315e74371e85e54e1e35e745c14045e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 13 Apr 2026 19:57:47 +0000 Subject: [PATCH 2/9] feat(adt-mcp): add 12 new tools for feature parity with vibing-steampunk (#H1-#H8) New tools: - grep_objects: regex search within named ABAP objects - grep_packages: regex search across a package (and subpackages) - get_table: DDIC table/structure definition reader - get_table_contents: read table data with WHERE filter + row limit - run_query: execute freestyle ABAP SQL SELECT queries - find_definition: navigate to symbol definition via ADT navigation endpoint - find_references: find all usages (where-used) via ADT usages endpoint - get_callers_of: call hierarchy upward traversal - get_callees_of: call hierarchy downward traversal - create_object: create ABAP objects (PROG, CLAS, INTF, FUGR, DEVC) - delete_object: delete ABAP objects using typed CRUD contracts - activate_package: batch-activate all inactive objects in a package Also completes: - cts_create_transport: now fully implemented via transportrequests.create() - cts_release_transport: now fully implemented via POST ?_action=RELEASE Co-authored: updated mock fixtures, routes and integration tests for all new tools Agent-Logs-Url: https://github.com/abapify/adt-cli/sessions/5086b608-c242-4318-a434-645c3114edbd Co-authored-by: ThePlenkov <6381507+ThePlenkov@users.noreply.github.com> --- packages/adt-mcp/src/lib/mock/fixtures.ts | 134 +++++++++ packages/adt-mcp/src/lib/mock/server.ts | 136 +++++++++- .../adt-mcp/src/lib/tools/activate-package.ts | 120 +++++++++ .../adt-mcp/src/lib/tools/create-object.ts | 170 ++++++++++++ .../src/lib/tools/cts-create-transport.ts | 76 +++++- .../src/lib/tools/cts-release-transport.ts | 49 +++- .../adt-mcp/src/lib/tools/delete-object.ts | 102 +++++++ .../adt-mcp/src/lib/tools/find-definition.ts | 111 ++++++++ .../adt-mcp/src/lib/tools/find-references.ts | 109 ++++++++ .../adt-mcp/src/lib/tools/get-callees-of.ts | 106 ++++++++ .../adt-mcp/src/lib/tools/get-callers-of.ts | 106 ++++++++ .../src/lib/tools/get-table-contents.ts | 97 +++++++ packages/adt-mcp/src/lib/tools/get-table.ts | 51 ++++ .../adt-mcp/src/lib/tools/grep-objects.ts | 111 ++++++++ .../adt-mcp/src/lib/tools/grep-packages.ts | 90 +++++++ packages/adt-mcp/src/lib/tools/index.ts | 27 ++ packages/adt-mcp/src/lib/tools/run-query.ts | 90 +++++++ packages/adt-mcp/tests/integration.test.ts | 255 ++++++++++++++++++ 18 files changed, 1915 insertions(+), 25 deletions(-) create mode 100644 packages/adt-mcp/src/lib/tools/activate-package.ts create mode 100644 packages/adt-mcp/src/lib/tools/create-object.ts create mode 100644 packages/adt-mcp/src/lib/tools/delete-object.ts create mode 100644 packages/adt-mcp/src/lib/tools/find-definition.ts create mode 100644 packages/adt-mcp/src/lib/tools/find-references.ts create mode 100644 packages/adt-mcp/src/lib/tools/get-callees-of.ts create mode 100644 packages/adt-mcp/src/lib/tools/get-callers-of.ts create mode 100644 packages/adt-mcp/src/lib/tools/get-table-contents.ts create mode 100644 packages/adt-mcp/src/lib/tools/get-table.ts create mode 100644 packages/adt-mcp/src/lib/tools/grep-objects.ts create mode 100644 packages/adt-mcp/src/lib/tools/grep-packages.ts create mode 100644 packages/adt-mcp/src/lib/tools/run-query.ts diff --git a/packages/adt-mcp/src/lib/mock/fixtures.ts b/packages/adt-mcp/src/lib/mock/fixtures.ts index 3adba5c5..40c24410 100644 --- a/packages/adt-mcp/src/lib/mock/fixtures.ts +++ b/packages/adt-mcp/src/lib/mock/fixtures.ts @@ -101,6 +101,19 @@ export const fixtures = { }, }, + // Transport create response – returned for POST /sap/bc/adt/cts/transportrequests + transportCreate: { + root: { + request: { + trkorr: 'DEVK900099', + as4text: 'New transport', + as4user: 'DEVELOPER', + trstatus: 'D', + trfunction: 'K', + }, + }, + }, + atcRun: { worklistId: 'WL_001', id: 'RUN_001', @@ -177,4 +190,125 @@ export const fixtures = { ], }, }, + + // Grep / content search results – returned for GET .../informationsystem/search?userannotation=userwhere + grepResults: { + objectReference: [ + { + name: 'ZCL_EXAMPLE', + type: 'CLAS/OC', + uri: '/sap/bc/adt/oo/classes/zcl_example', + description: 'Example class', + packageName: 'ZPACKAGE', + }, + ], + }, + + // DDIC table definition – returned for GET /sap/bc/adt/ddic/tables/{name} + tableDefinition: { + blueSource: { + name: 'MARA', + description: 'General Material Data', + type: 'TABL/DT', + element: [ + { name: 'MANDT', type: 'CLNT', length: '3', description: 'Client' }, + { + name: 'MATNR', + type: 'CHAR', + length: '18', + description: 'Material Number', + }, + { + name: 'MBRSH', + type: 'CHAR', + length: '1', + description: 'Industry Sector', + }, + ], + }, + }, + + // Data preview result – returned for POST /sap/bc/adt/datapreview/freestyle + tableContents: { + columns: { + column: [ + { name: 'MANDT', type: 'C', length: '3' }, + { name: 'MATNR', type: 'C', length: '18' }, + { name: 'MBRSH', type: 'C', length: '1' }, + ], + }, + rows: { + row: [ + { + cell: [ + { _text: '100' }, + { _text: 'Z_EXAMPLE_MATERIAL' }, + { _text: 'A' }, + ], + }, + ], + }, + }, + + // Navigation target – returned for GET /sap/bc/adt/navigation/target + navigationTarget: { + objectReference: { + uri: '/sap/bc/adt/oo/classes/zcl_example', + type: 'CLAS/OC', + name: 'ZCL_EXAMPLE', + description: 'Example class', + }, + }, + + // Usages / references – returned for GET .../informationsystem/usages + usagesResult: { + usages: { + usage: [ + { + uri: '/sap/bc/adt/programs/programs/zprog_example', + name: 'ZPROG_EXAMPLE', + type: 'PROG', + location: 'line 42', + }, + ], + }, + }, + + // Call hierarchy callers – returned for GET .../informationsystem/callers + callersResult: { + callers: { + caller: [ + { + uri: '/sap/bc/adt/programs/programs/zprog_main', + name: 'ZPROG_MAIN', + type: 'PROG', + }, + ], + }, + }, + + // Call hierarchy callees – returned for GET .../informationsystem/callees + calleesResult: { + callees: { + callee: [ + { + uri: '/sap/bc/adt/functions/groups/zfugr_util', + name: 'ZFUGR_UTIL', + type: 'FUGR', + }, + ], + }, + }, + + // Inactive objects – returned for GET /sap/bc/adt/activation/inactive_objects + inactiveObjects: { + objectReference: [ + { + name: 'ZCL_EXAMPLE', + type: 'CLAS/OC', + uri: '/sap/bc/adt/oo/classes/zcl_example', + description: 'Example class', + }, + ], + }, }; diff --git a/packages/adt-mcp/src/lib/mock/server.ts b/packages/adt-mcp/src/lib/mock/server.ts index 50ef44c3..db8c6fde 100644 --- a/packages/adt-mcp/src/lib/mock/server.ts +++ b/packages/adt-mcp/src/lib/mock/server.ts @@ -59,7 +59,20 @@ function matchRoute( }; } - // Quick search + // Grep / content search (userannotation=userwhere) – must come before general search + if ( + m === 'GET' && + url.startsWith('/sap/bc/adt/repository/informationsystem/search') && + url.includes('userannotation=userwhere') + ) { + return { + status: 200, + body: JSON.stringify(fixtures.grepResults), + contentType: 'application/json', + }; + } + + // Quick search (general – name pattern) if ( m === 'GET' && url.startsWith('/sap/bc/adt/repository/informationsystem/search') @@ -71,6 +84,91 @@ function matchRoute( }; } + // Usages / find-references + if ( + m === 'GET' && + url.startsWith('/sap/bc/adt/repository/informationsystem/usages') + ) { + return { + status: 200, + body: JSON.stringify(fixtures.usagesResult), + contentType: 'application/json', + }; + } + + // Call hierarchy – callers + if ( + m === 'GET' && + url.startsWith('/sap/bc/adt/repository/informationsystem/callers') + ) { + return { + status: 200, + body: JSON.stringify(fixtures.callersResult), + contentType: 'application/json', + }; + } + + // Call hierarchy – callees + if ( + m === 'GET' && + url.startsWith('/sap/bc/adt/repository/informationsystem/callees') + ) { + return { + status: 200, + body: JSON.stringify(fixtures.calleesResult), + contentType: 'application/json', + }; + } + + // Navigation target – find definition + if (m === 'GET' && url.startsWith('/sap/bc/adt/navigation/target')) { + return { + status: 200, + body: JSON.stringify(fixtures.navigationTarget), + contentType: 'application/json', + }; + } + + // Data preview – get_table_contents and run_query + if (m === 'POST' && url.startsWith('/sap/bc/adt/datapreview/freestyle')) { + return { + status: 200, + body: JSON.stringify(fixtures.tableContents), + contentType: 'application/json', + }; + } + + // DDIC tables – get_table (specific path, before generic DDIC) + if (m === 'GET' && url.startsWith('/sap/bc/adt/ddic/tables/')) { + return { + status: 200, + body: JSON.stringify(fixtures.tableDefinition), + contentType: 'application/json', + }; + } + + // CTS – create transport + if ( + m === 'POST' && + url.startsWith('/sap/bc/adt/cts/transportrequests') && + !url.includes('/sap/bc/adt/cts/transportrequests/') + ) { + return { + status: 200, + body: JSON.stringify(fixtures.transportCreate), + contentType: 'application/json', + }; + } + + // CTS – release transport (_action=RELEASE) + if ( + m === 'POST' && + /\/sap\/bc\/adt\/cts\/transportrequests\/\w+/.test(url) && + url.includes('_action=RELEASE') + ) { + return { status: 200, body: '', contentType: 'text/plain' }; + } + // CTS – list transports if ( m === 'GET' && @@ -160,6 +258,18 @@ function matchRoute( return { status: 200, body: '', contentType: 'text/plain' }; } + // Inactive objects – GET /sap/bc/adt/activation/inactive_objects + if ( + m === 'GET' && + url.startsWith('/sap/bc/adt/activation/inactive_objects') + ) { + return { + status: 200, + body: JSON.stringify(fixtures.inactiveObjects), + contentType: 'application/json', + }; + } + // Activation – POST /sap/bc/adt/activation if (m === 'POST' && url.startsWith('/sap/bc/adt/activation')) { return { @@ -169,6 +279,30 @@ function matchRoute( }; } + // Object create – POST to object-type paths (programs, classes, interfaces, functions, packages) + if ( + m === 'POST' && + (url.startsWith('/sap/bc/adt/programs/programs') || + url.startsWith('/sap/bc/adt/oo/classes') || + url.startsWith('/sap/bc/adt/oo/interfaces') || + url.startsWith('/sap/bc/adt/functions/groups') || + url.startsWith('/sap/bc/adt/packages')) + ) { + return { status: 200, body: '', contentType: 'text/plain' }; + } + + // Object delete – DELETE to object-type paths + if ( + m === 'DELETE' && + (url.startsWith('/sap/bc/adt/programs/programs/') || + url.startsWith('/sap/bc/adt/oo/classes/') || + url.startsWith('/sap/bc/adt/oo/interfaces/') || + url.startsWith('/sap/bc/adt/functions/groups/') || + url.startsWith('/sap/bc/adt/packages/')) + ) { + return { status: 204, body: '', contentType: 'text/plain' }; + } + // Syntax check – POST /sap/bc/adt/checkruns if (m === 'POST' && url.startsWith('/sap/bc/adt/checkruns')) { return { diff --git a/packages/adt-mcp/src/lib/tools/activate-package.ts b/packages/adt-mcp/src/lib/tools/activate-package.ts new file mode 100644 index 00000000..104346ed --- /dev/null +++ b/packages/adt-mcp/src/lib/tools/activate-package.ts @@ -0,0 +1,120 @@ +/** + * Tool: activate_package – batch-activate all inactive objects in a package + * + * 1. Lists inactive objects in the package via GET /sap/bc/adt/activation/inactive_objects + * 2. Activates them all in a single batch POST to /sap/bc/adt/activation + */ + +import { z } from 'zod'; +import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import type { ToolContext } from '../types'; +import { connectionShape } from './shared-schemas'; +import { extractObjectReferences } from './utils'; +import type { InferTypedSchema } from '@abapify/adt-schemas'; +import { adtcore } from '@abapify/adt-schemas'; + +type ObjectReferencesBody = Extract< + InferTypedSchema, + { objectReferences: unknown } +>; + +export function registerActivatePackageTool( + server: McpServer, + ctx: ToolContext, +): void { + server.tool( + 'activate_package', + 'Batch-activate all inactive objects in a package. Returns the count and list of activated objects.', + { + ...connectionShape, + packageName: z + .string() + .describe('ABAP package name (e.g. ZPACKAGE)'), + }, + async (args) => { + try { + const client = ctx.getClient(args); + const packageName = args.packageName.toUpperCase(); + + // Step 1: Get list of inactive objects in the package + const params = new URLSearchParams({ packageName }); + const inactiveResult = await client.fetch( + `/sap/bc/adt/activation/inactive_objects?${params.toString()}`, + { + method: 'GET', + headers: { Accept: 'application/json' }, + }, + ); + + const objects = extractObjectReferences(inactiveResult); + + if (objects.length === 0) { + return { + content: [ + { + type: 'text' as const, + text: JSON.stringify( + { + status: 'no_inactive_objects', + packageName, + message: 'No inactive objects found in this package', + }, + null, + 2, + ), + }, + ], + }; + } + + // Step 2: Activate all inactive objects in a single batch call + const body: ObjectReferencesBody = { + objectReferences: { + objectReference: objects.map((o) => ({ + uri: o.uri ?? '', + type: o.type ?? '', + name: (o.name ?? '').toUpperCase(), + })), + }, + }; + + await client.adt.activation.activate.post( + { method: 'activate', preauditRequested: true }, + body, + ); + + return { + content: [ + { + type: 'text' as const, + text: JSON.stringify( + { + status: 'activated', + packageName, + count: objects.length, + objects: objects.map((o) => ({ + name: o.name, + type: o.type, + uri: o.uri, + })), + }, + null, + 2, + ), + }, + ], + }; + } catch (error) { + return { + isError: true, + content: [ + { + type: 'text' as const, + text: `Activate package failed: ${error instanceof Error ? error.message : String(error)}`, + }, + ], + }; + } + }, + ); +} diff --git a/packages/adt-mcp/src/lib/tools/create-object.ts b/packages/adt-mcp/src/lib/tools/create-object.ts new file mode 100644 index 00000000..e6006153 --- /dev/null +++ b/packages/adt-mcp/src/lib/tools/create-object.ts @@ -0,0 +1,170 @@ +/** + * Tool: create_object – create a new ABAP object + * + * Supports creating the most common ABAP object types using the typed ADT contracts: + * PROG (program), CLAS (class), INTF (interface), FUGR (function group), DEVC (package) + * + * Uses the respective typed CRUD contract for each object type so that all + * XML serialisation goes through the schema pipeline (no manual XML building). + */ + +import { z } from 'zod'; +import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import type { ToolContext } from '../types'; +import { connectionShape } from './shared-schemas'; + +/** Object types supported by this tool */ +const SUPPORTED_TYPES = ['PROG', 'CLAS', 'INTF', 'FUGR', 'DEVC'] as const; +type SupportedType = (typeof SUPPORTED_TYPES)[number]; + +function isSupportedType(t: string): t is SupportedType { + return SUPPORTED_TYPES.includes(t.toUpperCase() as SupportedType); +} + +export function registerCreateObjectTool( + server: McpServer, + ctx: ToolContext, +): void { + server.tool( + 'create_object', + 'Create a new ABAP object. Supported types: PROG (program), CLAS (class), INTF (interface), FUGR (function group), DEVC (package)', + { + ...connectionShape, + objectName: z + .string() + .describe( + 'Name of the new object (uppercase, e.g. ZCL_MY_CLASS, ZPACKAGE)', + ), + objectType: z + .string() + .describe( + 'Object type: PROG, CLAS, INTF, FUGR, or DEVC', + ), + description: z.string().describe('Short description of the object'), + packageName: z + .string() + .optional() + .describe( + 'Package to assign the object to (required for non-local objects)', + ), + transport: z + .string() + .optional() + .describe( + 'Transport request number (required for transportable objects)', + ), + }, + async (args) => { + try { + const client = ctx.getClient(args); + const objectType = args.objectType.toUpperCase(); + const objectName = args.objectName.toUpperCase(); + + if (!isSupportedType(objectType)) { + return { + isError: true, + content: [ + { + type: 'text' as const, + text: `Object type '${objectType}' is not supported. Supported types: ${SUPPORTED_TYPES.join(', ')}`, + }, + ], + }; + } + + const packageRef = args.packageName + ? { uri: `/sap/bc/adt/packages/${args.packageName.toUpperCase()}` } + : undefined; + + const queryOptions = args.transport ? { corrNr: args.transport } : {}; + + const commonFields = { + name: objectName, + description: args.description, + language: 'EN', + masterLanguage: 'EN', + ...(packageRef ? { packageRef } : {}), + }; + + switch (objectType) { + case 'PROG': + await client.adt.programs.programs.post(queryOptions, { + abapProgram: { ...commonFields, type: 'PROG' }, + }); + break; + + case 'CLAS': + await client.adt.oo.classes.post(queryOptions, { + abapClass: { ...commonFields, type: 'CLAS/OC' }, + }); + break; + + case 'INTF': + await client.adt.oo.interfaces.post(queryOptions, { + abapInterface: { ...commonFields, type: 'INTF/OI' }, + }); + break; + + case 'FUGR': + await client.adt.functions.groups.post(queryOptions, { + abapFunctionGroup: { ...commonFields, type: 'FUGR' }, + }); + break; + + case 'DEVC': { + const pkgBody = { + package: { + name: objectName, + type: 'DEVC/K', + description: args.description, + language: 'EN', + masterLanguage: 'EN', + attributes: { packageType: 'development' }, + superPackage: {}, + extensionAlias: {}, + switch: {}, + applicationComponent: {}, + transport: {}, + translation: {}, + useAccesses: {}, + packageInterfaces: {}, + subPackages: {}, + }, + }; + await client.adt.packages.post(queryOptions, pkgBody); + break; + } + } + + return { + content: [ + { + type: 'text' as const, + text: JSON.stringify( + { + status: 'created', + objectName, + objectType, + description: args.description, + packageName: args.packageName?.toUpperCase(), + }, + null, + 2, + ), + }, + ], + }; + } catch (error) { + return { + isError: true, + content: [ + { + type: 'text' as const, + text: `Create object failed: ${error instanceof Error ? error.message : String(error)}`, + }, + ], + }; + } + }, + ); +} diff --git a/packages/adt-mcp/src/lib/tools/cts-create-transport.ts b/packages/adt-mcp/src/lib/tools/cts-create-transport.ts index 27451f33..a4ba32fb 100644 --- a/packages/adt-mcp/src/lib/tools/cts-create-transport.ts +++ b/packages/adt-mcp/src/lib/tools/cts-create-transport.ts @@ -3,18 +3,22 @@ * * CLI equivalent: `adt cts tr create` * - * Note: The underlying ADT client transport service `create()` method is not - * yet implemented. This tool returns a clear error until it is. + * Uses the transportrequests.create() contract with the transportmanagmentCreate schema + * to build and POST the XML request body. */ import { z } from 'zod'; import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import type { ToolContext } from '../types'; import { connectionShape } from './shared-schemas'; +import type { InferTypedSchema } from '@abapify/adt-schemas'; +import { transportmanagmentCreate } from '@abapify/adt-schemas'; + +type CreateBody = InferTypedSchema; export function registerCtsCreateTransportTool( server: McpServer, - _ctx: ToolContext, + ctx: ToolContext, ): void { server.tool( 'cts_create_transport', @@ -28,19 +32,65 @@ export function registerCtsCreateTransportTool( .describe( 'Transport type: K (Workbench) or W (Customizing). Default: K', ), - target: z.string().optional().describe('Target system (default: LOCAL)'), + target: z.string().optional().describe('Target system'), project: z.string().optional().describe('CTS project name'), }, - async (_args) => { - return { - isError: true, - content: [ - { - type: 'text' as const, - text: 'Create transport is not supported: the underlying ADT client transport service "create" method is not yet implemented.', + async (args) => { + try { + const client = ctx.getClient(args); + + const body: CreateBody = { + root: { + request: { + desc: args.description, + type: args.type ?? 'K', + ...(args.target ? { target: args.target } : {}), + ...(args.project ? { cts_project: args.project } : {}), + }, }, - ], - }; + }; + + const response = await client.adt.cts.transportrequests.create(body); + + // Extract transport number from response + const data = response as Record; + const request = + (data.root as Record)?.request ?? + data.request ?? + data; + const trkorr = + (request as Record)?.trkorr ?? + (request as Record)?.number ?? + ''; + + return { + content: [ + { + type: 'text' as const, + text: JSON.stringify( + { + status: 'created', + transport: String(trkorr), + description: args.description, + type: args.type ?? 'K', + }, + null, + 2, + ), + }, + ], + }; + } catch (error) { + return { + isError: true, + content: [ + { + type: 'text' as const, + text: `Create transport failed: ${error instanceof Error ? error.message : String(error)}`, + }, + ], + }; + } }, ); } diff --git a/packages/adt-mcp/src/lib/tools/cts-release-transport.ts b/packages/adt-mcp/src/lib/tools/cts-release-transport.ts index ee68c427..839c884c 100644 --- a/packages/adt-mcp/src/lib/tools/cts-release-transport.ts +++ b/packages/adt-mcp/src/lib/tools/cts-release-transport.ts @@ -3,8 +3,7 @@ * * CLI equivalent: `adt cts tr release ` * - * Note: The underlying ADT client transport service `release()` method is not - * yet implemented. This tool returns a clear error until it is. + * POSTs to /sap/bc/adt/cts/transportrequests/{trkorr}?_action=RELEASE */ import { z } from 'zod'; @@ -14,7 +13,7 @@ import { connectionShape } from './shared-schemas'; export function registerCtsReleaseTransportTool( server: McpServer, - _ctx: ToolContext, + ctx: ToolContext, ): void { server.tool( 'cts_release_transport', @@ -25,16 +24,44 @@ export function registerCtsReleaseTransportTool( .string() .describe('Transport number to release (e.g. S0DK900001)'), }, - async (_args) => { - return { - isError: true, - content: [ + async (args) => { + try { + const client = ctx.getClient(args); + + await client.fetch( + `/sap/bc/adt/cts/transportrequests/${args.transport}?_action=RELEASE`, { - type: 'text' as const, - text: 'Transport release is not supported: the ADT client transports.release() method is not yet implemented.', + method: 'POST', + headers: { + 'Content-Type': 'application/vnd.sap.adt.transportorganizer.v1+xml', + Accept: 'application/vnd.sap.adt.transportorganizer.v1+xml', + }, }, - ], - }; + ); + + return { + content: [ + { + type: 'text' as const, + text: JSON.stringify( + { status: 'released', transport: args.transport }, + null, + 2, + ), + }, + ], + }; + } catch (error) { + return { + isError: true, + content: [ + { + type: 'text' as const, + text: `Release transport failed: ${error instanceof Error ? error.message : String(error)}`, + }, + ], + }; + } }, ); } diff --git a/packages/adt-mcp/src/lib/tools/delete-object.ts b/packages/adt-mcp/src/lib/tools/delete-object.ts new file mode 100644 index 00000000..c436b594 --- /dev/null +++ b/packages/adt-mcp/src/lib/tools/delete-object.ts @@ -0,0 +1,102 @@ +/** + * Tool: delete_object – delete an ABAP object + * + * Deletes an ABAP object using the appropriate typed CRUD contract. + * Supports the same types as create_object: PROG, CLAS, INTF, FUGR, DEVC. + * + * For other object types, falls back to resolving the URI and using + * a direct DELETE request. + */ + +import { z } from 'zod'; +import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import type { ToolContext } from '../types'; +import { connectionShape } from './shared-schemas'; +import { resolveObjectUri } from './utils'; + +export function registerDeleteObjectTool( + server: McpServer, + ctx: ToolContext, +): void { + server.tool( + 'delete_object', + 'Delete an ABAP object. Supports PROG, CLAS, INTF, FUGR, DEVC and falls back to direct URI deletion for other types.', + { + ...connectionShape, + objectName: z.string().describe('Name of the ABAP object to delete'), + objectType: z + .string() + .optional() + .describe('Object type (e.g. CLAS, PROG, INTF, FUGR, DEVC)'), + transport: z + .string() + .optional() + .describe('Transport request number (required for transportable objects)'), + }, + async (args) => { + try { + const client = ctx.getClient(args); + const objectName = args.objectName.toUpperCase(); + const objectType = args.objectType?.toUpperCase(); + const queryOptions = args.transport ? { corrNr: args.transport } : {}; + const nameLower = objectName.toLowerCase(); + + // Use typed CRUD contracts for known types + if (objectType === 'PROG') { + await client.adt.programs.programs.delete(nameLower, queryOptions); + } else if (objectType === 'CLAS') { + await client.adt.oo.classes.delete(nameLower, queryOptions); + } else if (objectType === 'INTF') { + await client.adt.oo.interfaces.delete(nameLower, queryOptions); + } else if (objectType === 'FUGR') { + await client.adt.functions.groups.delete(nameLower, queryOptions); + } else if (objectType === 'DEVC') { + // Packages contract uses case-sensitive names + await client.adt.packages.delete(objectName, queryOptions); + } else { + // Fall back to resolving the URI and issuing a raw DELETE + const uri = await resolveObjectUri(client, objectName, objectType); + if (!uri) { + return { + isError: true, + content: [ + { + type: 'text' as const, + text: `Object '${objectName}' not found`, + }, + ], + }; + } + + const params = args.transport + ? `?corrNr=${encodeURIComponent(args.transport)}` + : ''; + await client.fetch(`${uri}${params}`, { method: 'DELETE' }); + } + + return { + content: [ + { + type: 'text' as const, + text: JSON.stringify( + { status: 'deleted', objectName, objectType }, + null, + 2, + ), + }, + ], + }; + } catch (error) { + return { + isError: true, + content: [ + { + type: 'text' as const, + text: `Delete object failed: ${error instanceof Error ? error.message : String(error)}`, + }, + ], + }; + } + }, + ); +} diff --git a/packages/adt-mcp/src/lib/tools/find-definition.ts b/packages/adt-mcp/src/lib/tools/find-definition.ts new file mode 100644 index 00000000..8454aed8 --- /dev/null +++ b/packages/adt-mcp/src/lib/tools/find-definition.ts @@ -0,0 +1,111 @@ +/** + * Tool: find_definition – navigate to the definition of an ABAP symbol + * + * Uses the ADT navigation endpoint to resolve where a symbol (class, method, + * data element, etc.) is defined. + * + * ADT endpoint: GET /sap/bc/adt/navigation/target + */ + +import { z } from 'zod'; +import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import type { ToolContext } from '../types'; +import { connectionShape } from './shared-schemas'; +import { resolveObjectUri } from './utils'; + +export function registerFindDefinitionTool( + server: McpServer, + ctx: ToolContext, +): void { + server.tool( + 'find_definition', + 'Navigate to the definition of an ABAP symbol (class, method, type, function module, etc.)', + { + ...connectionShape, + objectName: z + .string() + .describe('Name of the symbol or object to navigate to'), + objectType: z + .string() + .optional() + .describe( + 'Object type to narrow the search (e.g. CLAS, PROG, DTEL, TABL)', + ), + parentObjectName: z + .string() + .optional() + .describe( + 'Parent object name (e.g. class name when looking for a method)', + ), + parentObjectType: z + .string() + .optional() + .describe('Parent object type (e.g. CLAS)'), + }, + async (args) => { + try { + const client = ctx.getClient(args); + + const params = new URLSearchParams({ + objectName: args.objectName, + }); + + if (args.objectType) params.set('objectType', args.objectType); + if (args.parentObjectName) + params.set('context', args.parentObjectName); + if (args.parentObjectType) + params.set('contextType', args.parentObjectType); + + const result = await client.fetch( + `/sap/bc/adt/navigation/target?${params.toString()}`, + { + method: 'GET', + headers: { Accept: 'application/json' }, + }, + ); + + // If result is empty, try resolving via search + if (!result) { + const uri = await resolveObjectUri( + client, + args.objectName, + args.objectType, + ); + if (uri) { + return { + content: [ + { + type: 'text' as const, + text: JSON.stringify( + { objectName: args.objectName, uri }, + null, + 2, + ), + }, + ], + }; + } + } + + return { + content: [ + { + type: 'text' as const, + text: JSON.stringify(result, null, 2), + }, + ], + }; + } catch (error) { + return { + isError: true, + content: [ + { + type: 'text' as const, + text: `Find definition failed: ${error instanceof Error ? error.message : String(error)}`, + }, + ], + }; + } + }, + ); +} diff --git a/packages/adt-mcp/src/lib/tools/find-references.ts b/packages/adt-mcp/src/lib/tools/find-references.ts new file mode 100644 index 00000000..d9ce904d --- /dev/null +++ b/packages/adt-mcp/src/lib/tools/find-references.ts @@ -0,0 +1,109 @@ +/** + * Tool: find_references – find all usages (where-used) of an ABAP symbol + * + * Uses the ADT repository information system usages endpoint to find + * all references to a given object or symbol. + * + * ADT endpoint: GET /sap/bc/adt/repository/informationsystem/usages + */ + +import { z } from 'zod'; +import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import type { ToolContext } from '../types'; +import { connectionShape } from './shared-schemas'; +import { resolveObjectUri } from './utils'; + +export function registerFindReferencesTool( + server: McpServer, + ctx: ToolContext, +): void { + server.tool( + 'find_references', + 'Find all usages (where-used) of an ABAP object or symbol. Returns a list of locations where the object is referenced.', + { + ...connectionShape, + objectName: z + .string() + .describe('Name of the ABAP object to find references for'), + objectType: z + .string() + .optional() + .describe('Object type (e.g. CLAS, PROG, DTEL, TABL)'), + objectUri: z + .string() + .optional() + .describe( + 'Direct ADT URI of the object (skips name resolution if provided)', + ), + maxResults: z + .number() + .optional() + .describe('Maximum number of results (default: 100)'), + }, + async (args) => { + try { + const client = ctx.getClient(args); + const maxResults = args.maxResults ?? 100; + + // Resolve the object URI + let objectUri = args.objectUri; + if (!objectUri) { + objectUri = await resolveObjectUri( + client, + args.objectName, + args.objectType, + ); + if (!objectUri) { + return { + isError: true, + content: [ + { + type: 'text' as const, + text: `Object '${args.objectName}' not found`, + }, + ], + }; + } + } + + const params = new URLSearchParams({ + objectUri, + objectName: args.objectName, + maxResults: String(maxResults), + }); + if (args.objectType) params.set('objectType', args.objectType); + + const result = await client.fetch( + `/sap/bc/adt/repository/informationsystem/usages?${params.toString()}`, + { + method: 'GET', + headers: { Accept: 'application/json' }, + }, + ); + + return { + content: [ + { + type: 'text' as const, + text: JSON.stringify( + { objectName: args.objectName, objectUri, results: result }, + null, + 2, + ), + }, + ], + }; + } catch (error) { + return { + isError: true, + content: [ + { + type: 'text' as const, + text: `Find references failed: ${error instanceof Error ? error.message : String(error)}`, + }, + ], + }; + } + }, + ); +} diff --git a/packages/adt-mcp/src/lib/tools/get-callees-of.ts b/packages/adt-mcp/src/lib/tools/get-callees-of.ts new file mode 100644 index 00000000..bbd15b59 --- /dev/null +++ b/packages/adt-mcp/src/lib/tools/get-callees-of.ts @@ -0,0 +1,106 @@ +/** + * Tool: get_callees_of – find all callees of an ABAP method or function + * + * Uses the ADT repository information system callees endpoint to traverse + * the call hierarchy downward (what does this method/function call). + * + * ADT endpoint: GET /sap/bc/adt/repository/informationsystem/callees + */ + +import { z } from 'zod'; +import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import type { ToolContext } from '../types'; +import { connectionShape } from './shared-schemas'; +import { resolveObjectUri } from './utils'; + +export function registerGetCalleesOfTool( + server: McpServer, + ctx: ToolContext, +): void { + server.tool( + 'get_callees_of', + 'Find all callees (downward call hierarchy) of an ABAP method, function module, or subroutine', + { + ...connectionShape, + objectName: z + .string() + .describe('Name of the ABAP object (class, function group, program)'), + objectType: z + .string() + .optional() + .describe('Object type (e.g. CLAS, FUGR, PROG)'), + objectUri: z + .string() + .optional() + .describe( + 'Direct ADT URI of the object (skips name resolution if provided)', + ), + maxResults: z + .number() + .optional() + .describe('Maximum number of results (default: 50)'), + }, + async (args) => { + try { + const client = ctx.getClient(args); + const maxResults = args.maxResults ?? 50; + + let objectUri = args.objectUri; + if (!objectUri) { + objectUri = await resolveObjectUri( + client, + args.objectName, + args.objectType, + ); + if (!objectUri) { + return { + isError: true, + content: [ + { + type: 'text' as const, + text: `Object '${args.objectName}' not found`, + }, + ], + }; + } + } + + const params = new URLSearchParams({ + objectUri, + maxResults: String(maxResults), + }); + + const result = await client.fetch( + `/sap/bc/adt/repository/informationsystem/callees?${params.toString()}`, + { + method: 'GET', + headers: { Accept: 'application/json' }, + }, + ); + + return { + content: [ + { + type: 'text' as const, + text: JSON.stringify( + { objectName: args.objectName, objectUri, callees: result }, + null, + 2, + ), + }, + ], + }; + } catch (error) { + return { + isError: true, + content: [ + { + type: 'text' as const, + text: `Get callees failed: ${error instanceof Error ? error.message : String(error)}`, + }, + ], + }; + } + }, + ); +} diff --git a/packages/adt-mcp/src/lib/tools/get-callers-of.ts b/packages/adt-mcp/src/lib/tools/get-callers-of.ts new file mode 100644 index 00000000..8806be3a --- /dev/null +++ b/packages/adt-mcp/src/lib/tools/get-callers-of.ts @@ -0,0 +1,106 @@ +/** + * Tool: get_callers_of – find all callers of an ABAP method or function + * + * Uses the ADT repository information system callers endpoint to traverse + * the call hierarchy upward (who calls this method/function). + * + * ADT endpoint: GET /sap/bc/adt/repository/informationsystem/callers + */ + +import { z } from 'zod'; +import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import type { ToolContext } from '../types'; +import { connectionShape } from './shared-schemas'; +import { resolveObjectUri } from './utils'; + +export function registerGetCallersOfTool( + server: McpServer, + ctx: ToolContext, +): void { + server.tool( + 'get_callers_of', + 'Find all callers (upward call hierarchy) of an ABAP method, function module, or subroutine', + { + ...connectionShape, + objectName: z + .string() + .describe('Name of the ABAP object (class, function group, program)'), + objectType: z + .string() + .optional() + .describe('Object type (e.g. CLAS, FUGR, PROG)'), + objectUri: z + .string() + .optional() + .describe( + 'Direct ADT URI of the object (skips name resolution if provided)', + ), + maxResults: z + .number() + .optional() + .describe('Maximum number of results (default: 50)'), + }, + async (args) => { + try { + const client = ctx.getClient(args); + const maxResults = args.maxResults ?? 50; + + let objectUri = args.objectUri; + if (!objectUri) { + objectUri = await resolveObjectUri( + client, + args.objectName, + args.objectType, + ); + if (!objectUri) { + return { + isError: true, + content: [ + { + type: 'text' as const, + text: `Object '${args.objectName}' not found`, + }, + ], + }; + } + } + + const params = new URLSearchParams({ + objectUri, + maxResults: String(maxResults), + }); + + const result = await client.fetch( + `/sap/bc/adt/repository/informationsystem/callers?${params.toString()}`, + { + method: 'GET', + headers: { Accept: 'application/json' }, + }, + ); + + return { + content: [ + { + type: 'text' as const, + text: JSON.stringify( + { objectName: args.objectName, objectUri, callers: result }, + null, + 2, + ), + }, + ], + }; + } catch (error) { + return { + isError: true, + content: [ + { + type: 'text' as const, + text: `Get callers failed: ${error instanceof Error ? error.message : String(error)}`, + }, + ], + }; + } + }, + ); +} diff --git a/packages/adt-mcp/src/lib/tools/get-table-contents.ts b/packages/adt-mcp/src/lib/tools/get-table-contents.ts new file mode 100644 index 00000000..6de06690 --- /dev/null +++ b/packages/adt-mcp/src/lib/tools/get-table-contents.ts @@ -0,0 +1,97 @@ +/** + * Tool: get_table_contents – read table data with optional WHERE filter + * + * Uses the ADT data preview freestyle endpoint to execute a SELECT query + * against a DDIC table and return the result as JSON. + * + * ADT endpoint: POST /sap/bc/adt/datapreview/freestyle + */ + +import { z } from 'zod'; +import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import type { ToolContext } from '../types'; +import { connectionShape } from './shared-schemas'; + +export function registerGetTableContentsTool( + server: McpServer, + ctx: ToolContext, +): void { + server.tool( + 'get_table_contents', + 'Read data from a DDIC table with optional WHERE filter, column selection, and row limit', + { + ...connectionShape, + tableName: z + .string() + .describe('DDIC table name (e.g. MARA, VBAK, T001)'), + where: z + .string() + .optional() + .describe('WHERE clause (ABAP SQL syntax, e.g. "MATNR LIKE \'Z%\'"'), + columns: z + .array(z.string()) + .optional() + .describe( + 'Columns to select (default: all columns). Example: ["MATNR","MBRSH"]', + ), + maxRows: z + .number() + .optional() + .describe('Maximum rows to return (default: 100)'), + }, + async (args) => { + try { + const client = ctx.getClient(args); + const maxRows = args.maxRows ?? 100; + + const selectColumns = + args.columns && args.columns.length > 0 + ? args.columns.join(', ') + : '*'; + + const whereClause = args.where ? ` WHERE ${args.where}` : ''; + const query = `SELECT ${selectColumns} FROM ${args.tableName.toUpperCase()}${whereClause}`; + + const params = new URLSearchParams({ + rowCount: String(maxRows), + outputFormat: 'json', + }); + + const result = await client.fetch( + `/sap/bc/adt/datapreview/freestyle?${params.toString()}`, + { + method: 'POST', + headers: { + 'Content-Type': 'text/plain', + Accept: 'application/json', + }, + body: query, + }, + ); + + return { + content: [ + { + type: 'text' as const, + text: JSON.stringify( + { table: args.tableName.toUpperCase(), query, data: result }, + null, + 2, + ), + }, + ], + }; + } catch (error) { + return { + isError: true, + content: [ + { + type: 'text' as const, + text: `Get table contents failed: ${error instanceof Error ? error.message : String(error)}`, + }, + ], + }; + } + }, + ); +} diff --git a/packages/adt-mcp/src/lib/tools/get-table.ts b/packages/adt-mcp/src/lib/tools/get-table.ts new file mode 100644 index 00000000..d0dafda4 --- /dev/null +++ b/packages/adt-mcp/src/lib/tools/get-table.ts @@ -0,0 +1,51 @@ +/** + * Tool: get_table – read DDIC table or structure definition + * + * Fetches the table/structure metadata from the ADT DDIC tables endpoint. + * Uses the existing adt-contracts tables contract for typed access. + * + * ADT endpoint: /sap/bc/adt/ddic/tables/{name} + */ + +import { z } from 'zod'; +import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import type { ToolContext } from '../types'; +import { connectionShape } from './shared-schemas'; + +export function registerGetTableTool(server: McpServer, ctx: ToolContext): void { + server.tool( + 'get_table', + 'Read DDIC table or structure definition (fields, keys, data elements)', + { + ...connectionShape, + tableName: z.string().describe('DDIC table or structure name (e.g. MARA, VBAK)'), + }, + async (args) => { + try { + const client = ctx.getClient(args); + const name = args.tableName.toLowerCase(); + + const result = await client.adt.ddic.tables.get(name); + + return { + content: [ + { + type: 'text' as const, + text: JSON.stringify(result, null, 2), + }, + ], + }; + } catch (error) { + return { + isError: true, + content: [ + { + type: 'text' as const, + text: `Get table failed: ${error instanceof Error ? error.message : String(error)}`, + }, + ], + }; + } + }, + ); +} diff --git a/packages/adt-mcp/src/lib/tools/grep-objects.ts b/packages/adt-mcp/src/lib/tools/grep-objects.ts new file mode 100644 index 00000000..4413c9c0 --- /dev/null +++ b/packages/adt-mcp/src/lib/tools/grep-objects.ts @@ -0,0 +1,111 @@ +/** + * Tool: grep_objects – regex search within a list of ABAP object URIs + * + * Uses the ADT repository information system search endpoint with + * userannotation=userwhere for source code content search. + * + * ADT endpoint: /sap/bc/adt/repository/informationsystem/search + */ + +import { z } from 'zod'; +import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import type { ToolContext } from '../types'; +import { connectionShape } from './shared-schemas'; +import { resolveObjectUri } from './utils'; + +export function registerGrepObjectsTool( + server: McpServer, + ctx: ToolContext, +): void { + server.tool( + 'grep_objects', + 'Regex search for a pattern within ABAP object source code. Provide either a list of object URIs or name+type pairs to resolve them.', + { + ...connectionShape, + pattern: z + .string() + .describe('Search pattern (regex or literal string)'), + objectUris: z + .array(z.string()) + .optional() + .describe( + 'List of ADT object URIs to search within (e.g. /sap/bc/adt/oo/classes/zcl_example)', + ), + objects: z + .array( + z.object({ + objectName: z.string().describe('ABAP object name'), + objectType: z.string().describe('Object type (e.g. CLAS, PROG)'), + }), + ) + .optional() + .describe('Objects to search within (resolved to URIs automatically)'), + maxResults: z + .number() + .optional() + .describe('Maximum number of results (default: 50)'), + }, + async (args) => { + try { + const client = ctx.getClient(args); + const maxResults = args.maxResults ?? 50; + + // Resolve URIs from name/type pairs if provided + let uris: string[] = args.objectUris ?? []; + if (args.objects && args.objects.length > 0) { + for (const obj of args.objects) { + const uri = await resolveObjectUri( + client, + obj.objectName, + obj.objectType, + ); + if (uri) uris.push(uri); + } + } + + // Build query parameters + const params = new URLSearchParams({ + userannotation: 'userwhere', + query: args.pattern, + maxResults: String(maxResults), + }); + + // Add object URI references + uris.forEach((uri, i) => { + params.set(`objectReferences.${i}.uri`, uri); + }); + + const result = await client.fetch( + `/sap/bc/adt/repository/informationsystem/search?${params.toString()}`, + { + method: 'GET', + headers: { Accept: 'application/json' }, + }, + ); + + return { + content: [ + { + type: 'text' as const, + text: JSON.stringify( + { pattern: args.pattern, results: result }, + null, + 2, + ), + }, + ], + }; + } catch (error) { + return { + isError: true, + content: [ + { + type: 'text' as const, + text: `Grep objects failed: ${error instanceof Error ? error.message : String(error)}`, + }, + ], + }; + } + }, + ); +} diff --git a/packages/adt-mcp/src/lib/tools/grep-packages.ts b/packages/adt-mcp/src/lib/tools/grep-packages.ts new file mode 100644 index 00000000..12cb8203 --- /dev/null +++ b/packages/adt-mcp/src/lib/tools/grep-packages.ts @@ -0,0 +1,90 @@ +/** + * Tool: grep_packages – regex search across all objects in a package (and subpackages) + * + * Uses the ADT repository information system search endpoint with + * userannotation=userwhere and packageName parameter for package-scoped search. + * + * ADT endpoint: /sap/bc/adt/repository/informationsystem/search + */ + +import { z } from 'zod'; +import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import type { ToolContext } from '../types'; +import { connectionShape } from './shared-schemas'; + +export function registerGrepPackagesTool( + server: McpServer, + ctx: ToolContext, +): void { + server.tool( + 'grep_packages', + 'Regex search for a pattern across all ABAP source code within a package (and optionally its subpackages)', + { + ...connectionShape, + pattern: z + .string() + .describe('Search pattern (regex or literal string)'), + packageName: z + .string() + .describe('ABAP package name to search within (e.g. ZPACKAGE)'), + includeSubPackages: z + .boolean() + .optional() + .describe('Also search subpackages (default: true)'), + maxResults: z + .number() + .optional() + .describe('Maximum number of results (default: 50)'), + }, + async (args) => { + try { + const client = ctx.getClient(args); + const maxResults = args.maxResults ?? 50; + const includeSubPackages = args.includeSubPackages ?? true; + + const params = new URLSearchParams({ + userannotation: 'userwhere', + query: args.pattern, + maxResults: String(maxResults), + packageName: args.packageName.toUpperCase(), + includeSubpackages: String(includeSubPackages), + }); + + const result = await client.fetch( + `/sap/bc/adt/repository/informationsystem/search?${params.toString()}`, + { + method: 'GET', + headers: { Accept: 'application/json' }, + }, + ); + + return { + content: [ + { + type: 'text' as const, + text: JSON.stringify( + { + pattern: args.pattern, + packageName: args.packageName.toUpperCase(), + results: result, + }, + null, + 2, + ), + }, + ], + }; + } catch (error) { + return { + isError: true, + content: [ + { + type: 'text' as const, + text: `Grep packages failed: ${error instanceof Error ? error.message : String(error)}`, + }, + ], + }; + } + }, + ); +} diff --git a/packages/adt-mcp/src/lib/tools/index.ts b/packages/adt-mcp/src/lib/tools/index.ts index abf17ff1..47b76468 100644 --- a/packages/adt-mcp/src/lib/tools/index.ts +++ b/packages/adt-mcp/src/lib/tools/index.ts @@ -21,8 +21,22 @@ import { registerCheckSyntaxTool } from './check-syntax'; import { registerRunUnitTestsTool } from './run-unit-tests'; import { registerGetTestClassesTool } from './get-test-classes'; import { registerListPackageObjectsTool } from './list-package-objects'; +// New tools – high-priority feature parity (#H1–#H8) +import { registerGrepObjectsTool } from './grep-objects'; +import { registerGrepPackagesTool } from './grep-packages'; +import { registerGetTableTool } from './get-table'; +import { registerGetTableContentsTool } from './get-table-contents'; +import { registerRunQueryTool } from './run-query'; +import { registerFindDefinitionTool } from './find-definition'; +import { registerFindReferencesTool } from './find-references'; +import { registerGetCallersOfTool } from './get-callers-of'; +import { registerGetCalleesOfTool } from './get-callees-of'; +import { registerCreateObjectTool } from './create-object'; +import { registerDeleteObjectTool } from './delete-object'; +import { registerActivatePackageTool } from './activate-package'; export function registerTools(server: McpServer, ctx: ToolContext): void { + // Existing tools registerDiscoveryTool(server, ctx); registerSystemInfoTool(server, ctx); registerSearchObjectsTool(server, ctx); @@ -40,4 +54,17 @@ export function registerTools(server: McpServer, ctx: ToolContext): void { registerRunUnitTestsTool(server, ctx); registerGetTestClassesTool(server, ctx); registerListPackageObjectsTool(server, ctx); + // New tools – feature parity with vibing-steampunk + registerGrepObjectsTool(server, ctx); + registerGrepPackagesTool(server, ctx); + registerGetTableTool(server, ctx); + registerGetTableContentsTool(server, ctx); + registerRunQueryTool(server, ctx); + registerFindDefinitionTool(server, ctx); + registerFindReferencesTool(server, ctx); + registerGetCallersOfTool(server, ctx); + registerGetCalleesOfTool(server, ctx); + registerCreateObjectTool(server, ctx); + registerDeleteObjectTool(server, ctx); + registerActivatePackageTool(server, ctx); } diff --git a/packages/adt-mcp/src/lib/tools/run-query.ts b/packages/adt-mcp/src/lib/tools/run-query.ts new file mode 100644 index 00000000..42ac8bed --- /dev/null +++ b/packages/adt-mcp/src/lib/tools/run-query.ts @@ -0,0 +1,90 @@ +/** + * Tool: run_query – execute a freestyle ABAP SQL query + * + * Uses the ADT data preview freestyle endpoint to execute an arbitrary + * ABAP SQL SELECT query and return results as JSON. + * + * ADT endpoint: POST /sap/bc/adt/datapreview/freestyle + */ + +import { z } from 'zod'; +import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import type { ToolContext } from '../types'; +import { connectionShape } from './shared-schemas'; + +export function registerRunQueryTool( + server: McpServer, + ctx: ToolContext, +): void { + server.tool( + 'run_query', + 'Execute a freestyle ABAP SQL SELECT query and return results as JSON. Only SELECT statements are supported.', + { + ...connectionShape, + query: z + .string() + .describe( + 'ABAP SQL SELECT statement (e.g. "SELECT * FROM T001 WHERE MANDT = \'100\'")', + ), + maxRows: z + .number() + .optional() + .describe('Maximum rows to return (default: 100)'), + }, + async (args) => { + try { + const client = ctx.getClient(args); + const maxRows = args.maxRows ?? 100; + + const trimmedQuery = args.query.trim(); + if (!trimmedQuery.toUpperCase().startsWith('SELECT')) { + return { + isError: true, + content: [ + { + type: 'text' as const, + text: 'Only SELECT statements are supported by the data preview endpoint', + }, + ], + }; + } + + const params = new URLSearchParams({ + rowCount: String(maxRows), + outputFormat: 'json', + }); + + const result = await client.fetch( + `/sap/bc/adt/datapreview/freestyle?${params.toString()}`, + { + method: 'POST', + headers: { + 'Content-Type': 'text/plain', + Accept: 'application/json', + }, + body: trimmedQuery, + }, + ); + + return { + content: [ + { + type: 'text' as const, + text: JSON.stringify({ query: trimmedQuery, data: result }, null, 2), + }, + ], + }; + } catch (error) { + return { + isError: true, + content: [ + { + type: 'text' as const, + text: `Run query failed: ${error instanceof Error ? error.message : String(error)}`, + }, + ], + }; + } + }, + ); +} diff --git a/packages/adt-mcp/tests/integration.test.ts b/packages/adt-mcp/tests/integration.test.ts index a21c38c6..ecb25bef 100644 --- a/packages/adt-mcp/tests/integration.test.ts +++ b/packages/adt-mcp/tests/integration.test.ts @@ -413,6 +413,248 @@ describe('adt-mcp integration tests', () => { }); }); + // ── cts_create_transport ─────────────────────────────────────── + + describe('cts_create_transport tool', () => { + it('creates a transport and returns transport number', async () => { + const { json } = await callTool('cts_create_transport', { + ...connArgs(), + description: 'Test transport', + type: 'K', + }); + const data = json as { status: string; transport: string }; + assert.strictEqual(data.status, 'created'); + }); + }); + + // ── cts_release_transport ────────────────────────────────────── + + describe('cts_release_transport tool', () => { + it('releases a transport and returns status', async () => { + const { json } = await callTool('cts_release_transport', { + ...connArgs(), + transport: 'DEVK900001', + }); + const data = json as { status: string; transport: string }; + assert.strictEqual(data.status, 'released'); + assert.strictEqual(data.transport, 'DEVK900001'); + }); + }); + + // ── grep_objects ─────────────────────────────────────────────── + + describe('grep_objects tool', () => { + it('searches for a pattern within named objects', async () => { + const { json } = await callTool('grep_objects', { + ...connArgs(), + pattern: 'METHOD', + objects: [{ objectName: 'ZCL_EXAMPLE', objectType: 'CLAS' }], + }); + const data = json as { pattern: string; results: unknown }; + assert.strictEqual(data.pattern, 'METHOD'); + assert.ok(data.results !== undefined); + }); + }); + + // ── grep_packages ────────────────────────────────────────────── + + describe('grep_packages tool', () => { + it('searches for a pattern across a package', async () => { + const { json } = await callTool('grep_packages', { + ...connArgs(), + pattern: 'METHOD', + packageName: 'ZPACKAGE', + }); + const data = json as { pattern: string; packageName: string }; + assert.strictEqual(data.pattern, 'METHOD'); + assert.strictEqual(data.packageName, 'ZPACKAGE'); + }); + }); + + // ── get_table ────────────────────────────────────────────────── + + describe('get_table tool', () => { + it('returns DDIC table definition', async () => { + const { json } = await callTool('get_table', { + ...connArgs(), + tableName: 'MARA', + }); + assert.ok(json !== undefined, 'should return table definition'); + }); + }); + + // ── get_table_contents ───────────────────────────────────────── + + describe('get_table_contents tool', () => { + it('returns table data rows', async () => { + const { json } = await callTool('get_table_contents', { + ...connArgs(), + tableName: 'T001', + maxRows: 10, + }); + const data = json as { table: string; query: string }; + assert.strictEqual(data.table, 'T001'); + assert.ok(data.query.includes('SELECT')); + }); + }); + + // ── run_query ────────────────────────────────────────────────── + + describe('run_query tool', () => { + it('executes a SQL query and returns results', async () => { + const { json } = await callTool('run_query', { + ...connArgs(), + query: "SELECT * FROM T001 WHERE MANDT = '100'", + maxRows: 5, + }); + const data = json as { query: string }; + assert.ok(data.query.includes('SELECT')); + }); + + it('rejects non-SELECT statements', async () => { + const { raw } = await callTool('run_query', { + ...connArgs(), + query: "DELETE FROM T001 WHERE MANDT = '100'", + }); + const result = raw as { isError?: boolean }; + assert.ok(result.isError, 'should return error for non-SELECT statement'); + }); + }); + + // ── find_definition ──────────────────────────────────────────── + + describe('find_definition tool', () => { + it('returns navigation target for a symbol', async () => { + const { json } = await callTool('find_definition', { + ...connArgs(), + objectName: 'ZCL_EXAMPLE', + objectType: 'CLAS', + }); + assert.ok(json !== undefined, 'should return navigation target'); + }); + }); + + // ── find_references ──────────────────────────────────────────── + + describe('find_references tool', () => { + it('returns usages for an object', async () => { + const { json } = await callTool('find_references', { + ...connArgs(), + objectName: 'ZCL_EXAMPLE', + objectType: 'CLAS', + }); + const data = json as { objectName: string }; + assert.strictEqual(data.objectName, 'ZCL_EXAMPLE'); + }); + }); + + // ── get_callers_of ───────────────────────────────────────────── + + describe('get_callers_of tool', () => { + it('returns callers of an object', async () => { + const { json } = await callTool('get_callers_of', { + ...connArgs(), + objectName: 'ZCL_EXAMPLE', + objectType: 'CLAS', + }); + const data = json as { objectName: string; callers: unknown }; + assert.strictEqual(data.objectName, 'ZCL_EXAMPLE'); + assert.ok(data.callers !== undefined); + }); + }); + + // ── get_callees_of ───────────────────────────────────────────── + + describe('get_callees_of tool', () => { + it('returns callees of an object', async () => { + const { json } = await callTool('get_callees_of', { + ...connArgs(), + objectName: 'ZCL_EXAMPLE', + objectType: 'CLAS', + }); + const data = json as { objectName: string; callees: unknown }; + assert.strictEqual(data.objectName, 'ZCL_EXAMPLE'); + assert.ok(data.callees !== undefined); + }); + }); + + // ── create_object ────────────────────────────────────────────── + + describe('create_object tool', () => { + it('creates a CLAS object', async () => { + const { json } = await callTool('create_object', { + ...connArgs(), + objectName: 'ZCL_NEW', + objectType: 'CLAS', + description: 'New test class', + packageName: 'ZPACKAGE', + transport: 'DEVK900001', + }); + const data = json as { status: string; objectName: string; objectType: string }; + assert.strictEqual(data.status, 'created'); + assert.strictEqual(data.objectName, 'ZCL_NEW'); + assert.strictEqual(data.objectType, 'CLAS'); + }); + + it('creates a PROG object', async () => { + const { json } = await callTool('create_object', { + ...connArgs(), + objectName: 'ZPROG_NEW', + objectType: 'PROG', + description: 'New test program', + packageName: 'ZPACKAGE', + transport: 'DEVK900001', + }); + const data = json as { status: string; objectType: string }; + assert.strictEqual(data.status, 'created'); + assert.strictEqual(data.objectType, 'PROG'); + }); + + it('returns error for unsupported type', async () => { + const { raw } = await callTool('create_object', { + ...connArgs(), + objectName: 'ZFB01', + objectType: 'TRAN', + description: 'Unsupported type', + }); + const result = raw as { isError?: boolean }; + assert.ok(result.isError, 'should return error for unsupported type'); + }); + }); + + // ── delete_object ────────────────────────────────────────────── + + describe('delete_object tool', () => { + it('deletes a CLAS object', async () => { + const { json } = await callTool('delete_object', { + ...connArgs(), + objectName: 'ZCL_EXAMPLE', + objectType: 'CLAS', + transport: 'DEVK900001', + }); + const data = json as { status: string; objectName: string }; + assert.strictEqual(data.status, 'deleted'); + assert.strictEqual(data.objectName, 'ZCL_EXAMPLE'); + }); + }); + + // ── activate_package ─────────────────────────────────────────── + + describe('activate_package tool', () => { + it('activates all inactive objects in a package', async () => { + const { json } = await callTool('activate_package', { + ...connArgs(), + packageName: 'ZPACKAGE', + }); + const data = json as { status: string; packageName: string; count: number }; + assert.ok( + data.status === 'activated' || data.status === 'no_inactive_objects', + 'should return activated or no_inactive_objects status', + ); + assert.strictEqual(data.packageName, 'ZPACKAGE'); + }); + }); + // ── tool listing ─────────────────────────────────────────────── describe('tool listing', () => { @@ -437,6 +679,19 @@ describe('adt-mcp integration tests', () => { 'run_unit_tests', 'get_test_classes', 'list_package_objects', + // New tools – feature parity (#H1–#H8) + 'grep_objects', + 'grep_packages', + 'get_table', + 'get_table_contents', + 'run_query', + 'find_definition', + 'find_references', + 'get_callers_of', + 'get_callees_of', + 'create_object', + 'delete_object', + 'activate_package', ]; for (const name of expected) { assert.ok(names.has(name), `tool "${name}" should be listed`); From d7f8b4c9f5f3c8adb4629227859a51601edae698 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 13 Apr 2026 20:01:18 +0000 Subject: [PATCH 3/9] fix(adt-mcp): fix missing closing parenthesis in get-table-contents schema description Agent-Logs-Url: https://github.com/abapify/adt-cli/sessions/5086b608-c242-4318-a434-645c3114edbd Co-authored-by: ThePlenkov <6381507+ThePlenkov@users.noreply.github.com> --- packages/adt-mcp/src/lib/tools/get-table-contents.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/adt-mcp/src/lib/tools/get-table-contents.ts b/packages/adt-mcp/src/lib/tools/get-table-contents.ts index 6de06690..32403c87 100644 --- a/packages/adt-mcp/src/lib/tools/get-table-contents.ts +++ b/packages/adt-mcp/src/lib/tools/get-table-contents.ts @@ -27,7 +27,7 @@ export function registerGetTableContentsTool( where: z .string() .optional() - .describe('WHERE clause (ABAP SQL syntax, e.g. "MATNR LIKE \'Z%\'"'), + .describe('WHERE clause (ABAP SQL syntax, e.g. "MATNR LIKE \'Z%\'")'), columns: z .array(z.string()) .optional() From 170a2f688e9001da96837bb83621dfdd509195f0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 13 Apr 2026 20:55:18 +0000 Subject: [PATCH 4/9] feat(adt-mcp): implement 14 medium-priority MCP tools (#M1-#M10)" Add the following tools: - get_function_group, get_function (M1) - get_object_structure, get_type_hierarchy (M2) - lock_object, unlock_object (M3) - clone_object (M4) - pretty_print (M5) - create_package (M6) - get_installed_components, get_features (M8) - publish_service_binding (M9) - get_git_types, git_export (M10) Also add mock routes, fixtures, and integration tests for all new tools. Agent-Logs-Url: https://github.com/abapify/adt-cli/sessions/cde5b87d-03b6-4638-be0b-f5546f43eb15 Co-authored-by: ThePlenkov <6381507+ThePlenkov@users.noreply.github.com> --- packages/adt-mcp/src/lib/mock/fixtures.ts | 151 +++++++++++ packages/adt-mcp/src/lib/mock/server.ts | 99 +++++++ .../adt-mcp/src/lib/tools/clone-object.ts | 241 ++++++++++++++++++ .../adt-mcp/src/lib/tools/create-package.ts | 112 ++++++++ .../src/lib/tools/get-function-group.ts | 71 ++++++ .../adt-mcp/src/lib/tools/get-function.ts | 79 ++++++ .../src/lib/tools/get-installed-components.ts | 137 ++++++++++ .../src/lib/tools/get-object-structure.ts | 115 +++++++++ .../src/lib/tools/get-type-hierarchy.ts | 90 +++++++ packages/adt-mcp/src/lib/tools/git-tools.ts | 124 +++++++++ packages/adt-mcp/src/lib/tools/index.ts | 33 ++- packages/adt-mcp/src/lib/tools/lock-object.ts | 99 +++++++ .../adt-mcp/src/lib/tools/pretty-print.ts | 63 +++++ .../src/lib/tools/publish-service-binding.ts | 90 +++++++ .../adt-mcp/src/lib/tools/unlock-object.ts | 86 +++++++ packages/adt-mcp/tests/integration.test.ts | 239 ++++++++++++++++- 16 files changed, 1827 insertions(+), 2 deletions(-) create mode 100644 packages/adt-mcp/src/lib/tools/clone-object.ts create mode 100644 packages/adt-mcp/src/lib/tools/create-package.ts create mode 100644 packages/adt-mcp/src/lib/tools/get-function-group.ts create mode 100644 packages/adt-mcp/src/lib/tools/get-function.ts create mode 100644 packages/adt-mcp/src/lib/tools/get-installed-components.ts create mode 100644 packages/adt-mcp/src/lib/tools/get-object-structure.ts create mode 100644 packages/adt-mcp/src/lib/tools/get-type-hierarchy.ts create mode 100644 packages/adt-mcp/src/lib/tools/git-tools.ts create mode 100644 packages/adt-mcp/src/lib/tools/lock-object.ts create mode 100644 packages/adt-mcp/src/lib/tools/pretty-print.ts create mode 100644 packages/adt-mcp/src/lib/tools/publish-service-binding.ts create mode 100644 packages/adt-mcp/src/lib/tools/unlock-object.ts diff --git a/packages/adt-mcp/src/lib/mock/fixtures.ts b/packages/adt-mcp/src/lib/mock/fixtures.ts index 40c24410..055eb280 100644 --- a/packages/adt-mcp/src/lib/mock/fixtures.ts +++ b/packages/adt-mcp/src/lib/mock/fixtures.ts @@ -311,4 +311,155 @@ export const fixtures = { }, ], }, + + // Function group metadata – returned for GET /sap/bc/adt/functions/groups/{name} + functionGroup: { + abapFunctionGroup: { + name: 'ZFUGR_UTIL', + type: 'FUGR', + description: 'Utility function group', + language: 'EN', + masterLanguage: 'EN', + packageRef: { uri: '/sap/bc/adt/packages/zpackage' }, + }, + }, + + // Function module metadata – returned for GET /sap/bc/adt/functions/groups/{g}/fmodules/{fm} + functionModule: { + abapFunctionModule: { + name: 'Z_MY_FUNCTION', + type: 'FUGR/FF', + description: 'My utility function module', + processingType: 'normal', + remoteEnabledMode: 'notRemoteEnabled', + parameters: { + importParameters: { + parameter: [ + { + name: 'IV_INPUT', + type: 'TYPE', + associatedType: 'STRING', + optional: true, + }, + ], + }, + exportParameters: { + parameter: [ + { + name: 'EV_OUTPUT', + type: 'TYPE', + associatedType: 'STRING', + }, + ], + }, + }, + }, + }, + + // Object structure – returned for GET {objectUri}/objectstructure + objectStructure: { + objectStructure: { + objectReference: { + uri: '/sap/bc/adt/oo/classes/zcl_example', + type: 'CLAS/OC', + name: 'ZCL_EXAMPLE', + description: 'Example class', + }, + includes: { + include: [ + { + uri: '/sap/bc/adt/oo/classes/zcl_example/includes/definitions', + type: 'CLAS/OC/D', + name: 'ZCL_EXAMPLE', + }, + { + uri: '/sap/bc/adt/oo/classes/zcl_example/includes/implementations', + type: 'CLAS/OC/M', + name: 'ZCL_EXAMPLE', + }, + ], + }, + }, + }, + + // Type hierarchy – returned for GET /sap/bc/adt/oo/typeinfo + typeHierarchy: { + typeInfo: { + objectReference: { + uri: '/sap/bc/adt/oo/classes/zcl_example', + type: 'CLAS/OC', + name: 'ZCL_EXAMPLE', + }, + superClasses: { + superClass: [ + { + uri: '/sap/bc/adt/oo/classes/object', + type: 'CLAS/OC', + name: 'OBJECT', + }, + ], + }, + interfaces: { + interface: [], + }, + }, + }, + + // Pretty-printed source – returned for POST /sap/bc/adt/prettyprinter/prettifySource + prettySource: + 'CLASS zcl_example DEFINITION PUBLIC FINAL CREATE PUBLIC.\n PUBLIC SECTION.\n METHODS: do_something.\nENDCLASS.\n', + + // Installed software components – GET /sap/bc/adt/system/softwarecomponents + softwareComponents: { + softwareComponents: { + softwareComponent: [ + { + name: 'SAP_BASIS', + release: '756', + patchLevel: '0012', + description: 'SAP Basis Component', + }, + { + name: 'SAP_ABA', + release: '756', + patchLevel: '0012', + description: 'Cross-Application Component', + }, + ], + }, + }, + + // abapGit exportable objects – GET /sap/bc/adt/abapgit/objects + gitObjects: { + abapgitObjects: { + object: [ + { + name: 'ZCL_EXAMPLE', + type: 'CLAS', + uri: '/sap/bc/adt/oo/classes/zcl_example', + }, + { + name: 'ZPROG_EXAMPLE', + type: 'PROG', + uri: '/sap/bc/adt/programs/programs/zprog_example', + }, + ], + }, + }, + + // abapGit export – GET /sap/bc/adt/abapgit/repos/{name}/export + gitExport: { + files: [ + { + path: 'src/zcl_example.clas.abap', + content: + 'CLASS zcl_example DEFINITION PUBLIC FINAL CREATE PUBLIC.\nENDCLASS.', + }, + { + path: 'src/zcl_example.clas.xml', + content: + '', + }, + ], + }, }; diff --git a/packages/adt-mcp/src/lib/mock/server.ts b/packages/adt-mcp/src/lib/mock/server.ts index db8c6fde..2c44dd4a 100644 --- a/packages/adt-mcp/src/lib/mock/server.ts +++ b/packages/adt-mcp/src/lib/mock/server.ts @@ -321,6 +321,105 @@ function matchRoute( }; } + // Function module source – GET .../fmodules/{name}/source/main + if (m === 'GET' && url.includes('/fmodules/') && url.includes('/source/main')) { + return { + status: 200, + body: fixtures.sourceCode, + contentType: 'text/plain', + }; + } + + // Function modules metadata – GET /sap/bc/adt/functions/groups/{g}/fmodules/{fm} + if (m === 'GET' && url.includes('/fmodules/')) { + return { + status: 200, + body: JSON.stringify(fixtures.functionModule), + contentType: 'application/json', + }; + } + + // Function group source – GET /sap/bc/adt/functions/groups/{name}/source/main + if (m === 'GET' && url.startsWith('/sap/bc/adt/functions/groups/') && url.includes('/source/main')) { + return { + status: 200, + body: fixtures.sourceCode, + contentType: 'text/plain', + }; + } + + // Function group metadata – GET /sap/bc/adt/functions/groups/{name} + if (m === 'GET' && url.startsWith('/sap/bc/adt/functions/groups/') && !url.includes('?')) { + return { + status: 200, + body: JSON.stringify(fixtures.functionGroup), + contentType: 'application/json', + }; + } + + // Object structure – GET {objectUri}/objectstructure + if (m === 'GET' && url.includes('/objectstructure')) { + return { + status: 200, + body: JSON.stringify(fixtures.objectStructure), + contentType: 'application/json', + }; + } + + // Type hierarchy – GET /sap/bc/adt/oo/typeinfo + if (m === 'GET' && url.startsWith('/sap/bc/adt/oo/typeinfo')) { + return { + status: 200, + body: JSON.stringify(fixtures.typeHierarchy), + contentType: 'application/json', + }; + } + + // Pretty printer – POST /sap/bc/adt/prettyprinter/prettifySource + if (m === 'POST' && url.startsWith('/sap/bc/adt/prettyprinter/prettifySource')) { + return { + status: 200, + body: fixtures.prettySource, + contentType: 'text/plain', + }; + } + + // Software components – GET /sap/bc/adt/system/softwarecomponents + if (m === 'GET' && url.startsWith('/sap/bc/adt/system/softwarecomponents')) { + return { + status: 200, + body: JSON.stringify(fixtures.softwareComponents), + contentType: 'application/json', + }; + } + + // Service binding publish – POST/DELETE /sap/bc/adt/businessservices/bindings/{name}/publishedstates + if ( + (m === 'POST' || m === 'DELETE') && + url.includes('/sap/bc/adt/businessservices/bindings/') && + url.includes('/publishedstates') + ) { + return { status: 200, body: '', contentType: 'text/plain' }; + } + + // abapGit exportable objects – GET /sap/bc/adt/abapgit/objects + if (m === 'GET' && url.startsWith('/sap/bc/adt/abapgit/objects')) { + return { + status: 200, + body: JSON.stringify(fixtures.gitObjects), + contentType: 'application/json', + }; + } + + // abapGit export – GET /sap/bc/adt/abapgit/repos/{name}/export + if (m === 'GET' && url.includes('/sap/bc/adt/abapgit/repos/') && url.includes('/export')) { + return { + status: 200, + body: JSON.stringify(fixtures.gitExport), + contentType: 'application/json', + }; + } + // CSRF token fetch (used by write operations) if (m === 'HEAD') { return { diff --git a/packages/adt-mcp/src/lib/tools/clone-object.ts b/packages/adt-mcp/src/lib/tools/clone-object.ts new file mode 100644 index 00000000..12ccb2a4 --- /dev/null +++ b/packages/adt-mcp/src/lib/tools/clone-object.ts @@ -0,0 +1,241 @@ +/** + * Tool: clone_object – copy an ABAP object to a new name + * + * Creates a new object of the same type, copies the source code from the + * original, and optionally activates the clone. + * + * Supports PROG, CLAS, INTF (source-based objects). For other types falls + * back to an error asking for manual creation. + * + * ADT approach: create new object → copy source → activate clone + */ + +import { z } from 'zod'; +import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { createLockService } from '@abapify/adt-locks'; +import type { ToolContext } from '../types'; +import { connectionShape } from './shared-schemas'; +import { resolveObjectUri, resolveObjectUriFromType } from './utils'; + +const CLONABLE_TYPES = ['PROG', 'CLAS', 'INTF'] as const; +type ClonableType = (typeof CLONABLE_TYPES)[number]; + +function isClonableType(t: string): t is ClonableType { + return CLONABLE_TYPES.includes(t.toUpperCase() as ClonableType); +} + +async function getSourceCode( + client: ReturnType, + objectType: ClonableType, + objectName: string, +): Promise { + const name = objectName.toLowerCase(); + switch (objectType) { + case 'PROG': + return (await client.adt.programs.programs.source.main.get(name)) as string; + case 'CLAS': + return (await client.adt.oo.classes.source.main.get(name)) as string; + case 'INTF': + return (await client.adt.oo.interfaces.source.main.get(name)) as string; + } +} + +async function createNewObject( + client: ReturnType, + objectType: ClonableType, + objectName: string, + description: string, + packageName: string | undefined, + transport: string | undefined, +): Promise { + const packageRef = packageName + ? { uri: `/sap/bc/adt/packages/${packageName.toUpperCase()}` } + : undefined; + const queryOptions = transport ? { corrNr: transport } : {}; + const commonFields = { + name: objectName.toUpperCase(), + description, + language: 'EN', + masterLanguage: 'EN', + ...(packageRef ? { packageRef } : {}), + }; + + switch (objectType) { + case 'PROG': + await client.adt.programs.programs.post(queryOptions, { + abapProgram: { ...commonFields, type: 'PROG' }, + }); + break; + case 'CLAS': + await client.adt.oo.classes.post(queryOptions, { + abapClass: { ...commonFields, type: 'CLAS/OC' }, + }); + break; + case 'INTF': + await client.adt.oo.interfaces.post(queryOptions, { + abapInterface: { ...commonFields, type: 'INTF/OI' }, + }); + break; + } +} + +export function registerCloneObjectTool( + server: McpServer, + ctx: ToolContext, +): void { + server.tool( + 'clone_object', + 'Copy an ABAP object to a new name. Supported types: PROG, CLAS, INTF. Creates the new object and copies the source code.', + { + ...connectionShape, + sourceObjectName: z + .string() + .describe('Name of the source object to copy'), + sourceObjectType: z + .string() + .describe('Object type of the source: PROG, CLAS, or INTF'), + targetObjectName: z + .string() + .describe('Name for the new (cloned) object'), + targetDescription: z + .string() + .optional() + .describe('Description for the clone (defaults to source description with "Copy of" prefix)'), + targetPackage: z + .string() + .optional() + .describe('Package for the clone (defaults to same package as source)'), + transport: z + .string() + .optional() + .describe('Transport request number for the clone'), + }, + async (args) => { + try { + const client = ctx.getClient(args); + const sourceType = args.sourceObjectType.toUpperCase().split('/')[0]; + const sourceName = args.sourceObjectName.toUpperCase(); + const targetName = args.targetObjectName.toUpperCase(); + + if (!isClonableType(sourceType)) { + return { + isError: true, + content: [ + { + type: 'text' as const, + text: `Object type '${sourceType}' is not supported for cloning. Supported types: ${CLONABLE_TYPES.join(', ')}`, + }, + ], + }; + } + + // 1. Get source object metadata (for description) + const sourceUri = resolveObjectUriFromType(sourceType, sourceName); + let description = args.targetDescription ?? `Copy of ${sourceName}`; + if (!args.targetDescription && sourceUri) { + try { + const meta = (await client.fetch(sourceUri, { + method: 'GET', + headers: { Accept: 'application/json' }, + })) as Record; + const desc = (meta as Record>)?.abapClass?.description + ?? (meta as Record>)?.abapProgram?.description + ?? (meta as Record>)?.abapInterface?.description; + if (desc) description = `Copy of ${String(desc)}`; + } catch { + // ignore metadata fetch failure, use default description + } + } + + // 2. Get source code + const sourceCode = await getSourceCode(client, sourceType, sourceName); + + // 3. Create the target object + await createNewObject( + client, + sourceType, + targetName, + description, + args.targetPackage, + args.transport, + ); + + // 4. Copy the source code to the clone + const resolvedTargetUri = + resolveObjectUriFromType(sourceType, targetName) ?? + (await resolveObjectUri(client, targetName, sourceType)); + + if (!resolvedTargetUri) { + return { + isError: true, + content: [ + { + type: 'text' as const, + text: `Could not resolve URI for target object '${targetName}'`, + }, + ], + }; + } + + const lockService = createLockService(client); + const lockHandle = await lockService.lock(resolvedTargetUri, { + transport: args.transport, + objectName: targetName, + objectType: sourceType, + }); + + try { + const putParams = new URLSearchParams({ + lockHandle: lockHandle.handle, + ...(args.transport ? { corrNr: args.transport } : {}), + }); + + await client.fetch( + `${resolvedTargetUri}/source/main?${putParams.toString()}`, + { + method: 'PUT', + headers: { 'Content-Type': 'text/plain' }, + body: sourceCode as string, + }, + ); + } finally { + await lockService.unlock(resolvedTargetUri, { + lockHandle: lockHandle.handle, + }); + } + + return { + content: [ + { + type: 'text' as const, + text: JSON.stringify( + { + status: 'cloned', + sourceObject: { name: sourceName, type: sourceType }, + targetObject: { + name: targetName, + type: sourceType, + description, + uri: resolvedTargetUri, + }, + }, + null, + 2, + ), + }, + ], + }; + } catch (error) { + return { + isError: true, + content: [ + { + type: 'text' as const, + text: `Clone object failed: ${error instanceof Error ? error.message : String(error)}`, + }, + ], + }; + } + }, + ); +} diff --git a/packages/adt-mcp/src/lib/tools/create-package.ts b/packages/adt-mcp/src/lib/tools/create-package.ts new file mode 100644 index 00000000..4c6a12d9 --- /dev/null +++ b/packages/adt-mcp/src/lib/tools/create-package.ts @@ -0,0 +1,112 @@ +/** + * Tool: create_package – create a new ABAP development package (DEVC) + * + * Dedicated tool for package creation with package-specific options: + * - packageType: 'development' (default), 'structure', or 'main' + * - parentPackage: super-package URI + * - Transport (optional, omit for local $TMP packages) + * + * ADT endpoint: POST /sap/bc/adt/packages + */ + +import { z } from 'zod'; +import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import type { ToolContext } from '../types'; +import { connectionShape } from './shared-schemas'; + +export function registerCreatePackageTool( + server: McpServer, + ctx: ToolContext, +): void { + server.tool( + 'create_package', + 'Create a new ABAP development package (DEVC). Omit transport for local ($TMP) packages.', + { + ...connectionShape, + packageName: z + .string() + .describe('Package name (e.g. ZPACKAGE, $TMP_TEST)'), + description: z.string().describe('Short description of the package'), + parentPackage: z + .string() + .optional() + .describe( + 'Parent package name (e.g. ZROOT). If omitted the package is created at the top level.', + ), + packageType: z + .enum(['development', 'structure', 'main']) + .optional() + .describe('Package type (default: development)'), + transport: z + .string() + .optional() + .describe( + 'Transport request number. Omit for local packages (e.g. $-prefixed names).', + ), + }, + async (args) => { + try { + const client = ctx.getClient(args); + const packageName = args.packageName.toUpperCase(); + const packageType = args.packageType ?? 'development'; + + const queryOptions = args.transport ? { corrNr: args.transport } : {}; + + const pkgBody = { + package: { + name: packageName, + type: 'DEVC/K', + description: args.description, + language: 'EN', + masterLanguage: 'EN', + attributes: { packageType }, + superPackage: args.parentPackage + ? { + uri: `/sap/bc/adt/packages/${args.parentPackage.toUpperCase()}`, + } + : {}, + extensionAlias: {}, + switch: {}, + applicationComponent: {}, + transport: {}, + translation: {}, + useAccesses: {}, + packageInterfaces: {}, + subPackages: {}, + }, + }; + + await client.adt.packages.post(queryOptions, pkgBody); + + return { + content: [ + { + type: 'text' as const, + text: JSON.stringify( + { + status: 'created', + packageName, + packageType, + description: args.description, + parentPackage: args.parentPackage?.toUpperCase(), + }, + null, + 2, + ), + }, + ], + }; + } catch (error) { + return { + isError: true, + content: [ + { + type: 'text' as const, + text: `Create package failed: ${error instanceof Error ? error.message : String(error)}`, + }, + ], + }; + } + }, + ); +} diff --git a/packages/adt-mcp/src/lib/tools/get-function-group.ts b/packages/adt-mcp/src/lib/tools/get-function-group.ts new file mode 100644 index 00000000..d477e265 --- /dev/null +++ b/packages/adt-mcp/src/lib/tools/get-function-group.ts @@ -0,0 +1,71 @@ +/** + * Tool: get_function_group – read ABAP function group metadata and source + * + * Retrieves the function group metadata (description, includes) and + * optionally the main include source code. + * + * ADT endpoint: /sap/bc/adt/functions/groups/{groupName} + */ + +import { z } from 'zod'; +import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import type { ToolContext } from '../types'; +import { connectionShape } from './shared-schemas'; + +export function registerGetFunctionGroupTool( + server: McpServer, + ctx: ToolContext, +): void { + server.tool( + 'get_function_group', + 'Read ABAP function group metadata (description, includes). Optionally includes source code.', + { + ...connectionShape, + groupName: z + .string() + .describe('Function group name (e.g. ZFUGR_UTIL)'), + includeSource: z + .boolean() + .optional() + .describe('Whether to also return the main include source code (default: false)'), + }, + async (args) => { + try { + const client = ctx.getClient(args); + const name = args.groupName.toLowerCase(); + + const metadata = await client.adt.functions.groups.get(name); + + let source: string | undefined; + if (args.includeSource) { + source = (await client.adt.functions.groups.source.main.get( + name, + )) as string; + } + + return { + content: [ + { + type: 'text' as const, + text: JSON.stringify( + { metadata, ...(source !== undefined ? { source } : {}) }, + null, + 2, + ), + }, + ], + }; + } catch (error) { + return { + isError: true, + content: [ + { + type: 'text' as const, + text: `Get function group failed: ${error instanceof Error ? error.message : String(error)}`, + }, + ], + }; + } + }, + ); +} diff --git a/packages/adt-mcp/src/lib/tools/get-function.ts b/packages/adt-mcp/src/lib/tools/get-function.ts new file mode 100644 index 00000000..324421dc --- /dev/null +++ b/packages/adt-mcp/src/lib/tools/get-function.ts @@ -0,0 +1,79 @@ +/** + * Tool: get_function – read ABAP function module metadata and source + * + * Retrieves the function module metadata (signature, parameters, exceptions) + * and optionally the source code. + * + * ADT endpoint: /sap/bc/adt/functions/groups/{groupName}/fmodules/{fmName} + */ + +import { z } from 'zod'; +import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import type { ToolContext } from '../types'; +import { connectionShape } from './shared-schemas'; + +export function registerGetFunctionTool( + server: McpServer, + ctx: ToolContext, +): void { + server.tool( + 'get_function', + 'Read ABAP function module metadata (parameters, exceptions) and optionally its source code.', + { + ...connectionShape, + groupName: z + .string() + .describe('Function group name (e.g. ZFUGR_UTIL)'), + functionName: z + .string() + .describe('Function module name (e.g. Z_MY_FUNCTION)'), + includeSource: z + .boolean() + .optional() + .describe('Whether to also return the source code (default: false)'), + }, + async (args) => { + try { + const client = ctx.getClient(args); + const groupName = args.groupName.toLowerCase(); + const fmName = args.functionName.toLowerCase(); + + const metadata = await client.adt.functions.groups.fmodules.get( + groupName, + fmName, + ); + + let source: string | undefined; + if (args.includeSource) { + source = (await client.fetch( + `/sap/bc/adt/functions/groups/${groupName}/fmodules/${fmName}/source/main`, + { method: 'GET', headers: { Accept: 'text/plain' } }, + )) as string; + } + + return { + content: [ + { + type: 'text' as const, + text: JSON.stringify( + { metadata, ...(source !== undefined ? { source } : {}) }, + null, + 2, + ), + }, + ], + }; + } catch (error) { + return { + isError: true, + content: [ + { + type: 'text' as const, + text: `Get function failed: ${error instanceof Error ? error.message : String(error)}`, + }, + ], + }; + } + }, + ); +} diff --git a/packages/adt-mcp/src/lib/tools/get-installed-components.ts b/packages/adt-mcp/src/lib/tools/get-installed-components.ts new file mode 100644 index 00000000..b8c71ecb --- /dev/null +++ b/packages/adt-mcp/src/lib/tools/get-installed-components.ts @@ -0,0 +1,137 @@ +/** + * Tool: get_installed_components – list installed SAP software components + * + * Returns the list of software components installed on the SAP system + * together with their versions and release information. + * + * ADT endpoint: GET /sap/bc/adt/system/softwarecomponents + */ + +import { z } from 'zod'; +import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import type { ToolContext } from '../types'; +import { connectionShape } from './shared-schemas'; + +export function registerGetInstalledComponentsTool( + server: McpServer, + ctx: ToolContext, +): void { + server.tool( + 'get_installed_components', + 'List all software components installed on the SAP system with their version and release information.', + { + ...connectionShape, + }, + async (args) => { + try { + const client = ctx.getClient(args); + + const result = await client.fetch( + '/sap/bc/adt/system/softwarecomponents', + { + method: 'GET', + headers: { Accept: 'application/json' }, + }, + ); + + return { + content: [ + { + type: 'text' as const, + text: JSON.stringify(result, null, 2), + }, + ], + }; + } catch (error) { + return { + isError: true, + content: [ + { + type: 'text' as const, + text: `Get installed components failed: ${error instanceof Error ? error.message : String(error)}`, + }, + ], + }; + } + }, + ); +} + +/** + * Tool: get_features – probe the SAP system for available ADT features + * + * Checks the discovery endpoint and probes system-specific endpoints to + * determine which features are available: abapGit, RAP, AMDP, UI5, ATC, etc. + * + * ADT endpoint: GET /sap/bc/adt/discovery (augmented with feature probing) + */ +export function registerGetFeaturesTool( + server: McpServer, + ctx: ToolContext, +): void { + server.tool( + 'get_features', + 'Probe the SAP system for available ADT features (abapGit, RAP, AMDP, UI5, ATC, CTS, etc.).', + { + ...connectionShape, + }, + async (args) => { + try { + const client = ctx.getClient(args); + + // Fetch the discovery document to understand available services + const discovery = (await client.fetch('/sap/bc/adt/discovery', { + method: 'GET', + headers: { Accept: 'application/json' }, + })) as { workspaces?: Array<{ title: string; collections?: Array<{ href: string; title: string }> }> }; + + // Extract all service paths from the discovery document + const services = new Set(); + if (discovery?.workspaces) { + for (const ws of discovery.workspaces) { + for (const col of ws.collections ?? []) { + services.add(col.href); + } + } + } + + // Feature detection heuristics based on known ADT paths + const features: Record = { + atc: services.has('/sap/bc/adt/atc') || [...services].some((s) => s.includes('/atc')), + cts: [...services].some((s) => s.includes('/cts')), + aunit: [...services].some((s) => s.includes('/abapunit') || s.includes('/aunit')), + abapgit: [...services].some((s) => s.includes('/abapgit')), + rap: [...services].some((s) => s.includes('/businessservices') || s.includes('/rap')), + ui5: [...services].some((s) => s.includes('/ui5') || s.includes('/bsp')), + classicBadi: [...services].some((s) => s.includes('/enhancements')), + prettyPrinter: [...services].some((s) => s.includes('/prettyprinter')), + dataPreview: [...services].some((s) => s.includes('/datapreview')), + navigation: [...services].some((s) => s.includes('/navigation')), + }; + + return { + content: [ + { + type: 'text' as const, + text: JSON.stringify( + { features, discoveryServices: [...services].sort() }, + null, + 2, + ), + }, + ], + }; + } catch (error) { + return { + isError: true, + content: [ + { + type: 'text' as const, + text: `Get features failed: ${error instanceof Error ? error.message : String(error)}`, + }, + ], + }; + } + }, + ); +} diff --git a/packages/adt-mcp/src/lib/tools/get-object-structure.ts b/packages/adt-mcp/src/lib/tools/get-object-structure.ts new file mode 100644 index 00000000..19b2c8a9 --- /dev/null +++ b/packages/adt-mcp/src/lib/tools/get-object-structure.ts @@ -0,0 +1,115 @@ +/** + * Tool: get_object_structure – retrieve the structural tree of an ABAP object + * + * Returns the object explorer tree including includes, methods, attributes, + * and other sub-elements depending on the object type. + * + * ADT endpoint: GET {objectUri}/objectstructure + */ + +import { z } from 'zod'; +import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import type { ToolContext } from '../types'; +import { connectionShape } from './shared-schemas'; +import { resolveObjectUriFromType, resolveObjectUri } from './utils'; + +/** + * Dispatch to the appropriate typed CRUD contract's objectstructure() method + * based on the object type, or fall back to a raw fetch if unknown. + */ +async function fetchObjectStructure( + client: ReturnType, + objectName: string, + objectType: string | undefined, + version?: 'active' | 'inactive', +): Promise { + const name = objectName.toLowerCase(); + const type = objectType?.toUpperCase().split('/')[0]; + + const structureOptions = version ? { version } : {}; + + switch (type) { + case 'PROG': + return client.adt.programs.programs.objectstructure( + name, + structureOptions, + ); + case 'CLAS': + return client.adt.oo.classes.objectstructure(name, structureOptions); + case 'INTF': + return client.adt.oo.interfaces.objectstructure(name, structureOptions); + case 'FUGR': + return client.adt.functions.groups.objectstructure( + name, + structureOptions, + ); + default: { + // Generic fallback: resolve URI and fetch /objectstructure + const uri = + type && resolveObjectUriFromType(type, objectName) + ? resolveObjectUriFromType(type, objectName) + : await resolveObjectUri(client, objectName, objectType); + + if (!uri) throw new Error(`Object '${objectName}' not found`); + + const params = version ? `?version=${version}` : ''; + return client.fetch(`${uri}/objectstructure${params}`, { + method: 'GET', + headers: { Accept: 'application/json' }, + }); + } + } +} + +export function registerGetObjectStructureTool( + server: McpServer, + ctx: ToolContext, +): void { + server.tool( + 'get_object_structure', + 'Get the structural tree of an ABAP object (includes, methods, attributes, sub-components).', + { + ...connectionShape, + objectName: z.string().describe('ABAP object name'), + objectType: z + .string() + .optional() + .describe('Object type (e.g. CLAS, PROG, INTF, FUGR)'), + version: z + .enum(['active', 'inactive']) + .optional() + .describe('Object version to inspect (default: active)'), + }, + async (args) => { + try { + const client = ctx.getClient(args); + + const result = await fetchObjectStructure( + client, + args.objectName, + args.objectType, + args.version, + ); + + return { + content: [ + { + type: 'text' as const, + text: JSON.stringify(result, null, 2), + }, + ], + }; + } catch (error) { + return { + isError: true, + content: [ + { + type: 'text' as const, + text: `Get object structure failed: ${error instanceof Error ? error.message : String(error)}`, + }, + ], + }; + } + }, + ); +} diff --git a/packages/adt-mcp/src/lib/tools/get-type-hierarchy.ts b/packages/adt-mcp/src/lib/tools/get-type-hierarchy.ts new file mode 100644 index 00000000..583d56a6 --- /dev/null +++ b/packages/adt-mcp/src/lib/tools/get-type-hierarchy.ts @@ -0,0 +1,90 @@ +/** + * Tool: get_type_hierarchy – retrieve super/sub-types of an ABAP class or interface + * + * Returns the inheritance hierarchy (superclasses, interfaces implemented, + * and optionally subclasses) for a given class or interface. + * + * ADT endpoint: GET /sap/bc/adt/oo/typeinfo + */ + +import { z } from 'zod'; +import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import type { ToolContext } from '../types'; +import { connectionShape } from './shared-schemas'; + +export function registerGetTypeHierarchyTool( + server: McpServer, + ctx: ToolContext, +): void { + server.tool( + 'get_type_hierarchy', + 'Get the type hierarchy (super/sub-types, implemented interfaces) of an ABAP class or interface.', + { + ...connectionShape, + objectName: z + .string() + .describe('Class or interface name (e.g. ZCL_MY_CLASS, ZIF_MY_INTF)'), + objectType: z + .enum(['CLAS', 'INTF']) + .optional() + .describe('Object type: CLAS (class) or INTF (interface). Auto-detected if omitted.'), + includeSubTypes: z + .boolean() + .optional() + .describe('Whether to include sub-types (subclasses/implementors) in the result (default: false)'), + }, + async (args) => { + try { + const client = ctx.getClient(args); + const objectName = args.objectName.toUpperCase(); + + // Detect type from name if not provided (interfaces often start with ZIF_/IF_) + let objectType = args.objectType?.toUpperCase() ?? 'CLAS'; + if (!args.objectType) { + const upper = objectName.toUpperCase(); + if (upper.startsWith('IF_') || upper.startsWith('ZIF_')) { + objectType = 'INTF'; + } + } + + const expand = ['superClasses', 'interfaces']; + if (args.includeSubTypes) { + expand.push('subClasses'); + } + + const params = new URLSearchParams({ + type: objectType === 'INTF' ? 'INTF/OI' : 'CLAS/OC', + objectName, + expand: expand.join(','), + }); + + const result = await client.fetch( + `/sap/bc/adt/oo/typeinfo?${params.toString()}`, + { + method: 'GET', + headers: { Accept: 'application/json' }, + }, + ); + + return { + content: [ + { + type: 'text' as const, + text: JSON.stringify(result, null, 2), + }, + ], + }; + } catch (error) { + return { + isError: true, + content: [ + { + type: 'text' as const, + text: `Get type hierarchy failed: ${error instanceof Error ? error.message : String(error)}`, + }, + ], + }; + } + }, + ); +} diff --git a/packages/adt-mcp/src/lib/tools/git-tools.ts b/packages/adt-mcp/src/lib/tools/git-tools.ts new file mode 100644 index 00000000..51087a9e --- /dev/null +++ b/packages/adt-mcp/src/lib/tools/git-tools.ts @@ -0,0 +1,124 @@ +/** + * Tools: get_git_types + git_export – abapGit integration + * + * get_git_types: list ABAP objects in a package that can be exported via abapGit. + * git_export: export package contents in abapGit XML format (one file per object). + * + * Requires the abapGit ADT plugin installed on the SAP system. + * + * ADT endpoints: + * GET /sap/bc/adt/abapgit/objects?package={name} – list exportable objects + * GET /sap/bc/adt/abapgit/repos/{name}/export – export package + */ + +import { z } from 'zod'; +import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import type { ToolContext } from '../types'; +import { connectionShape } from './shared-schemas'; + +export function registerGetGitTypesTool( + server: McpServer, + ctx: ToolContext, +): void { + server.tool( + 'get_git_types', + 'List ABAP objects in a package that are eligible for abapGit export. Requires abapGit installed on the SAP system.', + { + ...connectionShape, + packageName: z + .string() + .describe('ABAP package name to inspect (e.g. ZPACKAGE)'), + }, + async (args) => { + try { + const client = ctx.getClient(args); + const packageName = args.packageName.toUpperCase(); + + const params = new URLSearchParams({ package: packageName }); + const result = await client.fetch( + `/sap/bc/adt/abapgit/objects?${params.toString()}`, + { + method: 'GET', + headers: { Accept: 'application/json' }, + }, + ); + + return { + content: [ + { + type: 'text' as const, + text: JSON.stringify( + { packageName, objects: result }, + null, + 2, + ), + }, + ], + }; + } catch (error) { + return { + isError: true, + content: [ + { + type: 'text' as const, + text: `Get git types failed: ${error instanceof Error ? error.message : String(error)}`, + }, + ], + }; + } + }, + ); +} + +export function registerGitExportTool( + server: McpServer, + ctx: ToolContext, +): void { + server.tool( + 'git_export', + 'Export an ABAP package in abapGit XML format. Requires abapGit installed on the SAP system. Returns a map of file paths to their content.', + { + ...connectionShape, + packageName: z + .string() + .describe('ABAP package name to export (e.g. ZPACKAGE)'), + }, + async (args) => { + try { + const client = ctx.getClient(args); + const packageName = args.packageName.toUpperCase(); + + const result = await client.fetch( + `/sap/bc/adt/abapgit/repos/${packageName}/export`, + { + method: 'GET', + headers: { Accept: 'application/json' }, + }, + ); + + return { + content: [ + { + type: 'text' as const, + text: JSON.stringify( + { packageName, export: result }, + null, + 2, + ), + }, + ], + }; + } catch (error) { + return { + isError: true, + content: [ + { + type: 'text' as const, + text: `Git export failed: ${error instanceof Error ? error.message : String(error)}`, + }, + ], + }; + } + }, + ); +} diff --git a/packages/adt-mcp/src/lib/tools/index.ts b/packages/adt-mcp/src/lib/tools/index.ts index 47b76468..fdf9d3fc 100644 --- a/packages/adt-mcp/src/lib/tools/index.ts +++ b/packages/adt-mcp/src/lib/tools/index.ts @@ -34,6 +34,22 @@ import { registerGetCalleesOfTool } from './get-callees-of'; import { registerCreateObjectTool } from './create-object'; import { registerDeleteObjectTool } from './delete-object'; import { registerActivatePackageTool } from './activate-package'; +// Medium-priority feature parity (#M1–#M10) +import { registerGetFunctionGroupTool } from './get-function-group'; +import { registerGetFunctionTool } from './get-function'; +import { registerLockObjectTool } from './lock-object'; +import { registerUnlockObjectTool } from './unlock-object'; +import { registerGetObjectStructureTool } from './get-object-structure'; +import { registerGetTypeHierarchyTool } from './get-type-hierarchy'; +import { registerPrettyPrintTool } from './pretty-print'; +import { registerCreatePackageTool } from './create-package'; +import { + registerGetInstalledComponentsTool, + registerGetFeaturesTool, +} from './get-installed-components'; +import { registerCloneObjectTool } from './clone-object'; +import { registerPublishServiceBindingTool } from './publish-service-binding'; +import { registerGetGitTypesTool, registerGitExportTool } from './git-tools'; export function registerTools(server: McpServer, ctx: ToolContext): void { // Existing tools @@ -54,7 +70,7 @@ export function registerTools(server: McpServer, ctx: ToolContext): void { registerRunUnitTestsTool(server, ctx); registerGetTestClassesTool(server, ctx); registerListPackageObjectsTool(server, ctx); - // New tools – feature parity with vibing-steampunk + // New tools – feature parity with vibing-steampunk (#H1–#H8) registerGrepObjectsTool(server, ctx); registerGrepPackagesTool(server, ctx); registerGetTableTool(server, ctx); @@ -67,4 +83,19 @@ export function registerTools(server: McpServer, ctx: ToolContext): void { registerCreateObjectTool(server, ctx); registerDeleteObjectTool(server, ctx); registerActivatePackageTool(server, ctx); + // Medium-priority tools (#M1–#M10) + registerGetFunctionGroupTool(server, ctx); + registerGetFunctionTool(server, ctx); + registerLockObjectTool(server, ctx); + registerUnlockObjectTool(server, ctx); + registerGetObjectStructureTool(server, ctx); + registerGetTypeHierarchyTool(server, ctx); + registerPrettyPrintTool(server, ctx); + registerCreatePackageTool(server, ctx); + registerGetInstalledComponentsTool(server, ctx); + registerGetFeaturesTool(server, ctx); + registerCloneObjectTool(server, ctx); + registerPublishServiceBindingTool(server, ctx); + registerGetGitTypesTool(server, ctx); + registerGitExportTool(server, ctx); } diff --git a/packages/adt-mcp/src/lib/tools/lock-object.ts b/packages/adt-mcp/src/lib/tools/lock-object.ts new file mode 100644 index 00000000..ee2663af --- /dev/null +++ b/packages/adt-mcp/src/lib/tools/lock-object.ts @@ -0,0 +1,99 @@ +/** + * Tool: lock_object – acquire an ADT edit lock on an ABAP object + * + * Returns the lock handle that must be passed to unlock_object and to + * update_source / other write operations that require a prior lock. + * + * Uses the LockService from @abapify/adt-locks for the full SAP security + * session / CSRF handshake required for stateful lock operations. + * + * ADT endpoint: POST {objectUri}?_action=LOCK + */ + +import { z } from 'zod'; +import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { createLockService } from '@abapify/adt-locks'; +import type { ToolContext } from '../types'; +import { connectionShape } from './shared-schemas'; +import { resolveObjectUri } from './utils'; + +export function registerLockObjectTool( + server: McpServer, + ctx: ToolContext, +): void { + server.tool( + 'lock_object', + 'Acquire an ADT edit lock on an ABAP object and return the lock handle needed for subsequent write operations.', + { + ...connectionShape, + objectName: z.string().describe('ABAP object name'), + objectType: z + .string() + .optional() + .describe('Object type (e.g. CLAS, PROG, INTF, FUGR)'), + transport: z + .string() + .optional() + .describe('Transport request number'), + }, + async (args) => { + try { + const client = ctx.getClient(args); + + const objectUri = await resolveObjectUri( + client, + args.objectName, + args.objectType, + ); + + if (!objectUri) { + return { + isError: true, + content: [ + { + type: 'text' as const, + text: `Object '${args.objectName}' not found`, + }, + ], + }; + } + + const lockService = createLockService(client); + const lockHandle = await lockService.lock(objectUri, { + transport: args.transport, + objectName: args.objectName, + objectType: args.objectType, + }); + + return { + content: [ + { + type: 'text' as const, + text: JSON.stringify( + { + status: 'locked', + objectName: args.objectName.toUpperCase(), + objectUri, + lockHandle: lockHandle.handle, + correlationNumber: lockHandle.correlationNumber, + }, + null, + 2, + ), + }, + ], + }; + } catch (error) { + return { + isError: true, + content: [ + { + type: 'text' as const, + text: `Lock object failed: ${error instanceof Error ? error.message : String(error)}`, + }, + ], + }; + } + }, + ); +} diff --git a/packages/adt-mcp/src/lib/tools/pretty-print.ts b/packages/adt-mcp/src/lib/tools/pretty-print.ts new file mode 100644 index 00000000..b97ed1be --- /dev/null +++ b/packages/adt-mcp/src/lib/tools/pretty-print.ts @@ -0,0 +1,63 @@ +/** + * Tool: pretty_print – format ABAP source code via the SAP pretty-printer + * + * Sends source code to the SAP ADT pretty-printer and returns the formatted + * version. Does not modify the object in the system. + * + * ADT endpoint: POST /sap/bc/adt/prettyprinter/prettifySource + */ + +import { z } from 'zod'; +import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import type { ToolContext } from '../types'; +import { connectionShape } from './shared-schemas'; + +export function registerPrettyPrintTool( + server: McpServer, + ctx: ToolContext, +): void { + server.tool( + 'pretty_print', + 'Format ABAP source code via the SAP pretty-printer. Returns the formatted code without modifying the object.', + { + ...connectionShape, + sourceCode: z.string().describe('ABAP source code to format'), + }, + async (args) => { + try { + const client = ctx.getClient(args); + + const result = await client.fetch( + '/sap/bc/adt/prettyprinter/prettifySource', + { + method: 'POST', + headers: { + 'Content-Type': 'text/plain', + Accept: 'text/plain', + }, + body: args.sourceCode, + }, + ); + + return { + content: [ + { + type: 'text' as const, + text: typeof result === 'string' ? result : JSON.stringify(result), + }, + ], + }; + } catch (error) { + return { + isError: true, + content: [ + { + type: 'text' as const, + text: `Pretty print failed: ${error instanceof Error ? error.message : String(error)}`, + }, + ], + }; + } + }, + ); +} diff --git a/packages/adt-mcp/src/lib/tools/publish-service-binding.ts b/packages/adt-mcp/src/lib/tools/publish-service-binding.ts new file mode 100644 index 00000000..e7a636b4 --- /dev/null +++ b/packages/adt-mcp/src/lib/tools/publish-service-binding.ts @@ -0,0 +1,90 @@ +/** + * Tool: publish_service_binding – publish an OData service binding in the SAP system + * + * Activates and publishes an OData V2 or V4 service binding, making it + * accessible via the SAP Gateway. Requires an existing SRVB object. + * + * ADT endpoint: POST /sap/bc/adt/businessservices/bindings/{name}/publishedstates + */ + +import { z } from 'zod'; +import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import type { ToolContext } from '../types'; +import { connectionShape } from './shared-schemas'; + +export function registerPublishServiceBindingTool( + server: McpServer, + ctx: ToolContext, +): void { + server.tool( + 'publish_service_binding', + 'Publish (activate) an OData service binding (SRVB) in SAP to make it accessible via the Gateway.', + { + ...connectionShape, + bindingName: z + .string() + .describe('Service binding name (e.g. ZUI_MYAPP_O4)'), + unpublish: z + .boolean() + .optional() + .describe('If true, unpublishes (deactivates) the service binding instead (default: false)'), + }, + async (args) => { + try { + const client = ctx.getClient(args); + const bindingName = args.bindingName.toUpperCase(); + const action = args.unpublish ? 'unpublish' : 'publish'; + + if (args.unpublish) { + // DELETE from publishedstates to unpublish + await client.fetch( + `/sap/bc/adt/businessservices/bindings/${bindingName}/publishedstates`, + { + method: 'DELETE', + headers: { Accept: 'application/json' }, + }, + ); + } else { + // POST to publishedstates to publish + await client.fetch( + `/sap/bc/adt/businessservices/bindings/${bindingName}/publishedstates`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + body: JSON.stringify({ bindingName }), + }, + ); + } + + return { + content: [ + { + type: 'text' as const, + text: JSON.stringify( + { + status: action === 'publish' ? 'published' : 'unpublished', + bindingName, + }, + null, + 2, + ), + }, + ], + }; + } catch (error) { + return { + isError: true, + content: [ + { + type: 'text' as const, + text: `Publish service binding failed: ${error instanceof Error ? error.message : String(error)}`, + }, + ], + }; + } + }, + ); +} diff --git a/packages/adt-mcp/src/lib/tools/unlock-object.ts b/packages/adt-mcp/src/lib/tools/unlock-object.ts new file mode 100644 index 00000000..1188a11b --- /dev/null +++ b/packages/adt-mcp/src/lib/tools/unlock-object.ts @@ -0,0 +1,86 @@ +/** + * Tool: unlock_object – release an ADT edit lock on an ABAP object + * + * Requires the lock handle returned by lock_object (or update_source). + * + * ADT endpoint: POST {objectUri}?_action=UNLOCK&lockHandle={handle} + */ + +import { z } from 'zod'; +import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { createLockService } from '@abapify/adt-locks'; +import type { ToolContext } from '../types'; +import { connectionShape } from './shared-schemas'; +import { resolveObjectUri } from './utils'; + +export function registerUnlockObjectTool( + server: McpServer, + ctx: ToolContext, +): void { + server.tool( + 'unlock_object', + 'Release an ADT edit lock acquired with lock_object. Requires the lockHandle returned by lock_object.', + { + ...connectionShape, + objectName: z.string().describe('ABAP object name'), + objectType: z + .string() + .optional() + .describe('Object type (e.g. CLAS, PROG, INTF, FUGR)'), + lockHandle: z.string().describe('Lock handle returned by lock_object'), + }, + async (args) => { + try { + const client = ctx.getClient(args); + + const objectUri = await resolveObjectUri( + client, + args.objectName, + args.objectType, + ); + + if (!objectUri) { + return { + isError: true, + content: [ + { + type: 'text' as const, + text: `Object '${args.objectName}' not found`, + }, + ], + }; + } + + const lockService = createLockService(client); + await lockService.unlock(objectUri, { lockHandle: args.lockHandle }); + + return { + content: [ + { + type: 'text' as const, + text: JSON.stringify( + { + status: 'unlocked', + objectName: args.objectName.toUpperCase(), + objectUri, + }, + null, + 2, + ), + }, + ], + }; + } catch (error) { + return { + isError: true, + content: [ + { + type: 'text' as const, + text: `Unlock object failed: ${error instanceof Error ? error.message : String(error)}`, + }, + ], + }; + } + }, + ); +} diff --git a/packages/adt-mcp/tests/integration.test.ts b/packages/adt-mcp/tests/integration.test.ts index ecb25bef..721450a8 100644 --- a/packages/adt-mcp/tests/integration.test.ts +++ b/packages/adt-mcp/tests/integration.test.ts @@ -655,6 +655,228 @@ describe('adt-mcp integration tests', () => { }); }); + // ── get_function_group ───────────────────────────────────────── + + describe('get_function_group tool', () => { + it('returns function group metadata', async () => { + const { json } = await callTool('get_function_group', { + ...connArgs(), + groupName: 'ZFUGR_UTIL', + }); + const data = json as { metadata: unknown }; + assert.ok(data.metadata, 'should return metadata'); + }); + + it('returns function group with source when includeSource is true', async () => { + const { json } = await callTool('get_function_group', { + ...connArgs(), + groupName: 'ZFUGR_UTIL', + includeSource: true, + }); + const data = json as { metadata: unknown; source: string }; + assert.ok(data.metadata, 'should return metadata'); + assert.ok(typeof data.source === 'string', 'should return source string'); + }); + }); + + // ── get_function ────────────────────────────────────────────── + + describe('get_function tool', () => { + it('returns function module metadata', async () => { + const { json } = await callTool('get_function', { + ...connArgs(), + groupName: 'ZFUGR_UTIL', + functionName: 'Z_MY_FUNCTION', + }); + const data = json as { metadata: unknown }; + assert.ok(data.metadata, 'should return metadata'); + }); + + it('returns function module with source when includeSource is true', async () => { + const { json } = await callTool('get_function', { + ...connArgs(), + groupName: 'ZFUGR_UTIL', + functionName: 'Z_MY_FUNCTION', + includeSource: true, + }); + const data = json as { metadata: unknown; source: string }; + assert.ok(data.metadata, 'should return metadata'); + assert.ok(typeof data.source === 'string', 'should return source string'); + }); + }); + + // ── lock_object ─────────────────────────────────────────────── + + describe('lock_object tool', () => { + it('acquires a lock and returns lock handle', async () => { + const { json } = await callTool('lock_object', { + ...connArgs(), + objectName: 'ZCL_EXAMPLE', + objectType: 'CLAS', + }); + const data = json as { status: string; lockHandle: string }; + assert.strictEqual(data.status, 'locked'); + assert.ok(typeof data.lockHandle === 'string', 'should return lockHandle'); + }); + }); + + // ── unlock_object ───────────────────────────────────────────── + + describe('unlock_object tool', () => { + it('releases a lock', async () => { + const { json } = await callTool('unlock_object', { + ...connArgs(), + objectName: 'ZCL_EXAMPLE', + objectType: 'CLAS', + lockHandle: 'MOCK_LOCK_HANDLE_001', + }); + const data = json as { status: string }; + assert.strictEqual(data.status, 'unlocked'); + }); + }); + + // ── get_object_structure ────────────────────────────────────── + + describe('get_object_structure tool', () => { + it('returns object structure for a CLAS', async () => { + const { json } = await callTool('get_object_structure', { + ...connArgs(), + objectName: 'ZCL_EXAMPLE', + objectType: 'CLAS', + }); + assert.ok(json, 'should return object structure'); + }); + }); + + // ── get_type_hierarchy ──────────────────────────────────────── + + describe('get_type_hierarchy tool', () => { + it('returns type hierarchy for a class', async () => { + const { json } = await callTool('get_type_hierarchy', { + ...connArgs(), + objectName: 'ZCL_EXAMPLE', + objectType: 'CLAS', + }); + assert.ok(json, 'should return type hierarchy'); + }); + }); + + // ── pretty_print ────────────────────────────────────────────── + + describe('pretty_print tool', () => { + it('returns formatted ABAP source code', async () => { + const { json } = await callTool('pretty_print', { + ...connArgs(), + sourceCode: 'class zcl_example definition.\nendclass.', + }); + assert.ok(typeof json === 'string', 'should return formatted source as string'); + }); + }); + + // ── create_package ──────────────────────────────────────────── + + describe('create_package tool', () => { + it('creates a package', async () => { + const { json } = await callTool('create_package', { + ...connArgs(), + packageName: 'ZNEWPKG', + description: 'New test package', + }); + const data = json as { status: string; packageName: string }; + assert.strictEqual(data.status, 'created'); + assert.strictEqual(data.packageName, 'ZNEWPKG'); + }); + }); + + // ── get_installed_components ────────────────────────────────── + + describe('get_installed_components tool', () => { + it('returns installed software components', async () => { + const { json } = await callTool('get_installed_components', connArgs()); + assert.ok(json, 'should return software components'); + }); + }); + + // ── get_features ────────────────────────────────────────────── + + describe('get_features tool', () => { + it('returns feature detection result', async () => { + const { json } = await callTool('get_features', connArgs()); + const data = json as { features: Record }; + assert.ok(data.features, 'should return features object'); + assert.strictEqual(typeof data.features.atc, 'boolean', 'features.atc should be boolean'); + }); + }); + + // ── clone_object ────────────────────────────────────────────── + + describe('clone_object tool', () => { + it('clones a CLAS object', async () => { + const { json } = await callTool('clone_object', { + ...connArgs(), + sourceObjectName: 'ZCL_EXAMPLE', + sourceObjectType: 'CLAS', + targetObjectName: 'ZCL_EXAMPLE_COPY', + targetDescription: 'Copy of ZCL_EXAMPLE', + }); + const data = json as { status: string; targetObject: { name: string } }; + assert.strictEqual(data.status, 'cloned'); + assert.strictEqual(data.targetObject.name, 'ZCL_EXAMPLE_COPY'); + }); + }); + + // ── publish_service_binding ─────────────────────────────────── + + describe('publish_service_binding tool', () => { + it('publishes a service binding', async () => { + const { json } = await callTool('publish_service_binding', { + ...connArgs(), + bindingName: 'ZUI_MYAPP_O4', + }); + const data = json as { status: string; bindingName: string }; + assert.strictEqual(data.status, 'published'); + assert.strictEqual(data.bindingName, 'ZUI_MYAPP_O4'); + }); + + it('unpublishes a service binding', async () => { + const { json } = await callTool('publish_service_binding', { + ...connArgs(), + bindingName: 'ZUI_MYAPP_O4', + unpublish: true, + }); + const data = json as { status: string }; + assert.strictEqual(data.status, 'unpublished'); + }); + }); + + // ── get_git_types ───────────────────────────────────────────── + + describe('get_git_types tool', () => { + it('returns abapGit-eligible objects', async () => { + const { json } = await callTool('get_git_types', { + ...connArgs(), + packageName: 'ZPACKAGE', + }); + const data = json as { packageName: string; objects: unknown }; + assert.strictEqual(data.packageName, 'ZPACKAGE'); + assert.ok(data.objects, 'should return objects'); + }); + }); + + // ── git_export ──────────────────────────────────────────────── + + describe('git_export tool', () => { + it('returns package export in abapGit format', async () => { + const { json } = await callTool('git_export', { + ...connArgs(), + packageName: 'ZPACKAGE', + }); + const data = json as { packageName: string; export: unknown }; + assert.strictEqual(data.packageName, 'ZPACKAGE'); + assert.ok(data.export, 'should return export data'); + }); + }); + // ── tool listing ─────────────────────────────────────────────── describe('tool listing', () => { @@ -679,7 +901,7 @@ describe('adt-mcp integration tests', () => { 'run_unit_tests', 'get_test_classes', 'list_package_objects', - // New tools – feature parity (#H1–#H8) + // High-priority tools (#H1–#H8) 'grep_objects', 'grep_packages', 'get_table', @@ -692,6 +914,21 @@ describe('adt-mcp integration tests', () => { 'create_object', 'delete_object', 'activate_package', + // Medium-priority tools (#M1–#M10) + 'get_function_group', + 'get_function', + 'lock_object', + 'unlock_object', + 'get_object_structure', + 'get_type_hierarchy', + 'pretty_print', + 'create_package', + 'get_installed_components', + 'get_features', + 'clone_object', + 'publish_service_binding', + 'get_git_types', + 'git_export', ]; for (const name of expected) { assert.ok(names.has(name), `tool "${name}" should be listed`); From 2818f49e938400452adf50f78e9dc4a6d3720b32 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 13 Apr 2026 21:17:16 +0000 Subject: [PATCH 5/9] fix(adt-mcp): format all new tool files + fix mock server route ordering - Run Prettier on 11 files that were missing proper formatting - Fix mock server: function group metadata route now excludes /objectstructure, /source/ and /fmodules/ sub-paths to prevent incorrect route interception Agent-Logs-Url: https://github.com/abapify/adt-cli/sessions/d06841ae-664d-4446-af93-fff153d27ac5 Co-authored-by: ThePlenkov <6381507+ThePlenkov@users.noreply.github.com> --- packages/adt-mcp/src/lib/mock/server.ts | 33 ++++++++++++++++--- .../adt-mcp/src/lib/tools/clone-object.ts | 22 ++++++++----- .../src/lib/tools/get-function-group.ts | 8 ++--- .../adt-mcp/src/lib/tools/get-function.ts | 4 +-- .../src/lib/tools/get-installed-components.ts | 27 +++++++++++---- .../src/lib/tools/get-type-hierarchy.ts | 8 +++-- packages/adt-mcp/src/lib/tools/git-tools.ts | 12 ++----- packages/adt-mcp/src/lib/tools/lock-object.ts | 5 +-- .../adt-mcp/src/lib/tools/pretty-print.ts | 3 +- .../src/lib/tools/publish-service-binding.ts | 4 ++- packages/adt-mcp/tests/integration.test.ts | 28 +++++++++++++--- 11 files changed, 105 insertions(+), 49 deletions(-) diff --git a/packages/adt-mcp/src/lib/mock/server.ts b/packages/adt-mcp/src/lib/mock/server.ts index 2c44dd4a..84450637 100644 --- a/packages/adt-mcp/src/lib/mock/server.ts +++ b/packages/adt-mcp/src/lib/mock/server.ts @@ -322,7 +322,11 @@ function matchRoute( } // Function module source – GET .../fmodules/{name}/source/main - if (m === 'GET' && url.includes('/fmodules/') && url.includes('/source/main')) { + if ( + m === 'GET' && + url.includes('/fmodules/') && + url.includes('/source/main') + ) { return { status: 200, body: fixtures.sourceCode, @@ -340,7 +344,11 @@ function matchRoute( } // Function group source – GET /sap/bc/adt/functions/groups/{name}/source/main - if (m === 'GET' && url.startsWith('/sap/bc/adt/functions/groups/') && url.includes('/source/main')) { + if ( + m === 'GET' && + url.startsWith('/sap/bc/adt/functions/groups/') && + url.includes('/source/main') + ) { return { status: 200, body: fixtures.sourceCode, @@ -349,7 +357,15 @@ function matchRoute( } // Function group metadata – GET /sap/bc/adt/functions/groups/{name} - if (m === 'GET' && url.startsWith('/sap/bc/adt/functions/groups/') && !url.includes('?')) { + // Must exclude objectstructure, source, and fmodule sub-paths + if ( + m === 'GET' && + url.startsWith('/sap/bc/adt/functions/groups/') && + !url.includes('?') && + !url.includes('/objectstructure') && + !url.includes('/source/') && + !url.includes('/fmodules/') + ) { return { status: 200, body: JSON.stringify(fixtures.functionGroup), @@ -376,7 +392,10 @@ function matchRoute( } // Pretty printer – POST /sap/bc/adt/prettyprinter/prettifySource - if (m === 'POST' && url.startsWith('/sap/bc/adt/prettyprinter/prettifySource')) { + if ( + m === 'POST' && + url.startsWith('/sap/bc/adt/prettyprinter/prettifySource') + ) { return { status: 200, body: fixtures.prettySource, @@ -412,7 +431,11 @@ function matchRoute( } // abapGit export – GET /sap/bc/adt/abapgit/repos/{name}/export - if (m === 'GET' && url.includes('/sap/bc/adt/abapgit/repos/') && url.includes('/export')) { + if ( + m === 'GET' && + url.includes('/sap/bc/adt/abapgit/repos/') && + url.includes('/export') + ) { return { status: 200, body: JSON.stringify(fixtures.gitExport), diff --git a/packages/adt-mcp/src/lib/tools/clone-object.ts b/packages/adt-mcp/src/lib/tools/clone-object.ts index 12ccb2a4..3dda15b0 100644 --- a/packages/adt-mcp/src/lib/tools/clone-object.ts +++ b/packages/adt-mcp/src/lib/tools/clone-object.ts @@ -32,7 +32,9 @@ async function getSourceCode( const name = objectName.toLowerCase(); switch (objectType) { case 'PROG': - return (await client.adt.programs.programs.source.main.get(name)) as string; + return (await client.adt.programs.programs.source.main.get( + name, + )) as string; case 'CLAS': return (await client.adt.oo.classes.source.main.get(name)) as string; case 'INTF': @@ -94,13 +96,13 @@ export function registerCloneObjectTool( sourceObjectType: z .string() .describe('Object type of the source: PROG, CLAS, or INTF'), - targetObjectName: z - .string() - .describe('Name for the new (cloned) object'), + targetObjectName: z.string().describe('Name for the new (cloned) object'), targetDescription: z .string() .optional() - .describe('Description for the clone (defaults to source description with "Copy of" prefix)'), + .describe( + 'Description for the clone (defaults to source description with "Copy of" prefix)', + ), targetPackage: z .string() .optional() @@ -138,9 +140,13 @@ export function registerCloneObjectTool( method: 'GET', headers: { Accept: 'application/json' }, })) as Record; - const desc = (meta as Record>)?.abapClass?.description - ?? (meta as Record>)?.abapProgram?.description - ?? (meta as Record>)?.abapInterface?.description; + const desc = + (meta as Record>)?.abapClass + ?.description ?? + (meta as Record>)?.abapProgram + ?.description ?? + (meta as Record>)?.abapInterface + ?.description; if (desc) description = `Copy of ${String(desc)}`; } catch { // ignore metadata fetch failure, use default description diff --git a/packages/adt-mcp/src/lib/tools/get-function-group.ts b/packages/adt-mcp/src/lib/tools/get-function-group.ts index d477e265..de3aff46 100644 --- a/packages/adt-mcp/src/lib/tools/get-function-group.ts +++ b/packages/adt-mcp/src/lib/tools/get-function-group.ts @@ -21,13 +21,13 @@ export function registerGetFunctionGroupTool( 'Read ABAP function group metadata (description, includes). Optionally includes source code.', { ...connectionShape, - groupName: z - .string() - .describe('Function group name (e.g. ZFUGR_UTIL)'), + groupName: z.string().describe('Function group name (e.g. ZFUGR_UTIL)'), includeSource: z .boolean() .optional() - .describe('Whether to also return the main include source code (default: false)'), + .describe( + 'Whether to also return the main include source code (default: false)', + ), }, async (args) => { try { diff --git a/packages/adt-mcp/src/lib/tools/get-function.ts b/packages/adt-mcp/src/lib/tools/get-function.ts index 324421dc..e4d0a47b 100644 --- a/packages/adt-mcp/src/lib/tools/get-function.ts +++ b/packages/adt-mcp/src/lib/tools/get-function.ts @@ -21,9 +21,7 @@ export function registerGetFunctionTool( 'Read ABAP function module metadata (parameters, exceptions) and optionally its source code.', { ...connectionShape, - groupName: z - .string() - .describe('Function group name (e.g. ZFUGR_UTIL)'), + groupName: z.string().describe('Function group name (e.g. ZFUGR_UTIL)'), functionName: z .string() .describe('Function module name (e.g. Z_MY_FUNCTION)'), diff --git a/packages/adt-mcp/src/lib/tools/get-installed-components.ts b/packages/adt-mcp/src/lib/tools/get-installed-components.ts index b8c71ecb..5a8359d3 100644 --- a/packages/adt-mcp/src/lib/tools/get-installed-components.ts +++ b/packages/adt-mcp/src/lib/tools/get-installed-components.ts @@ -83,7 +83,12 @@ export function registerGetFeaturesTool( const discovery = (await client.fetch('/sap/bc/adt/discovery', { method: 'GET', headers: { Accept: 'application/json' }, - })) as { workspaces?: Array<{ title: string; collections?: Array<{ href: string; title: string }> }> }; + })) as { + workspaces?: Array<{ + title: string; + collections?: Array<{ href: string; title: string }>; + }>; + }; // Extract all service paths from the discovery document const services = new Set(); @@ -97,14 +102,24 @@ export function registerGetFeaturesTool( // Feature detection heuristics based on known ADT paths const features: Record = { - atc: services.has('/sap/bc/adt/atc') || [...services].some((s) => s.includes('/atc')), + atc: + services.has('/sap/bc/adt/atc') || + [...services].some((s) => s.includes('/atc')), cts: [...services].some((s) => s.includes('/cts')), - aunit: [...services].some((s) => s.includes('/abapunit') || s.includes('/aunit')), + aunit: [...services].some( + (s) => s.includes('/abapunit') || s.includes('/aunit'), + ), abapgit: [...services].some((s) => s.includes('/abapgit')), - rap: [...services].some((s) => s.includes('/businessservices') || s.includes('/rap')), - ui5: [...services].some((s) => s.includes('/ui5') || s.includes('/bsp')), + rap: [...services].some( + (s) => s.includes('/businessservices') || s.includes('/rap'), + ), + ui5: [...services].some( + (s) => s.includes('/ui5') || s.includes('/bsp'), + ), classicBadi: [...services].some((s) => s.includes('/enhancements')), - prettyPrinter: [...services].some((s) => s.includes('/prettyprinter')), + prettyPrinter: [...services].some((s) => + s.includes('/prettyprinter'), + ), dataPreview: [...services].some((s) => s.includes('/datapreview')), navigation: [...services].some((s) => s.includes('/navigation')), }; diff --git a/packages/adt-mcp/src/lib/tools/get-type-hierarchy.ts b/packages/adt-mcp/src/lib/tools/get-type-hierarchy.ts index 583d56a6..5c5c1d0d 100644 --- a/packages/adt-mcp/src/lib/tools/get-type-hierarchy.ts +++ b/packages/adt-mcp/src/lib/tools/get-type-hierarchy.ts @@ -27,11 +27,15 @@ export function registerGetTypeHierarchyTool( objectType: z .enum(['CLAS', 'INTF']) .optional() - .describe('Object type: CLAS (class) or INTF (interface). Auto-detected if omitted.'), + .describe( + 'Object type: CLAS (class) or INTF (interface). Auto-detected if omitted.', + ), includeSubTypes: z .boolean() .optional() - .describe('Whether to include sub-types (subclasses/implementors) in the result (default: false)'), + .describe( + 'Whether to include sub-types (subclasses/implementors) in the result (default: false)', + ), }, async (args) => { try { diff --git a/packages/adt-mcp/src/lib/tools/git-tools.ts b/packages/adt-mcp/src/lib/tools/git-tools.ts index 51087a9e..61246513 100644 --- a/packages/adt-mcp/src/lib/tools/git-tools.ts +++ b/packages/adt-mcp/src/lib/tools/git-tools.ts @@ -47,11 +47,7 @@ export function registerGetGitTypesTool( content: [ { type: 'text' as const, - text: JSON.stringify( - { packageName, objects: result }, - null, - 2, - ), + text: JSON.stringify({ packageName, objects: result }, null, 2), }, ], }; @@ -100,11 +96,7 @@ export function registerGitExportTool( content: [ { type: 'text' as const, - text: JSON.stringify( - { packageName, export: result }, - null, - 2, - ), + text: JSON.stringify({ packageName, export: result }, null, 2), }, ], }; diff --git a/packages/adt-mcp/src/lib/tools/lock-object.ts b/packages/adt-mcp/src/lib/tools/lock-object.ts index ee2663af..2e97f63d 100644 --- a/packages/adt-mcp/src/lib/tools/lock-object.ts +++ b/packages/adt-mcp/src/lib/tools/lock-object.ts @@ -31,10 +31,7 @@ export function registerLockObjectTool( .string() .optional() .describe('Object type (e.g. CLAS, PROG, INTF, FUGR)'), - transport: z - .string() - .optional() - .describe('Transport request number'), + transport: z.string().optional().describe('Transport request number'), }, async (args) => { try { diff --git a/packages/adt-mcp/src/lib/tools/pretty-print.ts b/packages/adt-mcp/src/lib/tools/pretty-print.ts index b97ed1be..83af80a7 100644 --- a/packages/adt-mcp/src/lib/tools/pretty-print.ts +++ b/packages/adt-mcp/src/lib/tools/pretty-print.ts @@ -43,7 +43,8 @@ export function registerPrettyPrintTool( content: [ { type: 'text' as const, - text: typeof result === 'string' ? result : JSON.stringify(result), + text: + typeof result === 'string' ? result : JSON.stringify(result), }, ], }; diff --git a/packages/adt-mcp/src/lib/tools/publish-service-binding.ts b/packages/adt-mcp/src/lib/tools/publish-service-binding.ts index e7a636b4..97b864c7 100644 --- a/packages/adt-mcp/src/lib/tools/publish-service-binding.ts +++ b/packages/adt-mcp/src/lib/tools/publish-service-binding.ts @@ -27,7 +27,9 @@ export function registerPublishServiceBindingTool( unpublish: z .boolean() .optional() - .describe('If true, unpublishes (deactivates) the service binding instead (default: false)'), + .describe( + 'If true, unpublishes (deactivates) the service binding instead (default: false)', + ), }, async (args) => { try { diff --git a/packages/adt-mcp/tests/integration.test.ts b/packages/adt-mcp/tests/integration.test.ts index 721450a8..604690c6 100644 --- a/packages/adt-mcp/tests/integration.test.ts +++ b/packages/adt-mcp/tests/integration.test.ts @@ -590,7 +590,11 @@ describe('adt-mcp integration tests', () => { packageName: 'ZPACKAGE', transport: 'DEVK900001', }); - const data = json as { status: string; objectName: string; objectType: string }; + const data = json as { + status: string; + objectName: string; + objectType: string; + }; assert.strictEqual(data.status, 'created'); assert.strictEqual(data.objectName, 'ZCL_NEW'); assert.strictEqual(data.objectType, 'CLAS'); @@ -646,7 +650,11 @@ describe('adt-mcp integration tests', () => { ...connArgs(), packageName: 'ZPACKAGE', }); - const data = json as { status: string; packageName: string; count: number }; + const data = json as { + status: string; + packageName: string; + count: number; + }; assert.ok( data.status === 'activated' || data.status === 'no_inactive_objects', 'should return activated or no_inactive_objects status', @@ -716,7 +724,10 @@ describe('adt-mcp integration tests', () => { }); const data = json as { status: string; lockHandle: string }; assert.strictEqual(data.status, 'locked'); - assert.ok(typeof data.lockHandle === 'string', 'should return lockHandle'); + assert.ok( + typeof data.lockHandle === 'string', + 'should return lockHandle', + ); }); }); @@ -769,7 +780,10 @@ describe('adt-mcp integration tests', () => { ...connArgs(), sourceCode: 'class zcl_example definition.\nendclass.', }); - assert.ok(typeof json === 'string', 'should return formatted source as string'); + assert.ok( + typeof json === 'string', + 'should return formatted source as string', + ); }); }); @@ -804,7 +818,11 @@ describe('adt-mcp integration tests', () => { const { json } = await callTool('get_features', connArgs()); const data = json as { features: Record }; assert.ok(data.features, 'should return features object'); - assert.strictEqual(typeof data.features.atc, 'boolean', 'features.atc should be boolean'); + assert.strictEqual( + typeof data.features.atc, + 'boolean', + 'features.atc should be boolean', + ); }); }); From 0c6d97c3ea289f716f7f7d6386db197a3fa3e15f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 13 Apr 2026 21:33:19 +0000 Subject: [PATCH 6/9] fix(adt-mcp): resolve SonarQube quality gate failures MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Merge get-callers-of + get-callees-of into call-hierarchy.ts with shared fetchCallHierarchy helper — eliminates 138 duplicated lines - Merge get-function-group + get-function into function-tools.ts with shared formatMetadataResult helper — eliminates 58 duplicated lines + fixes S7735 negated conditions (source !== undefined → source === undefined) - get-installed-components.ts: remove unused 'z' import (S1128), use localeCompare for sort (S2871 CRITICAL — was causing reliability rating 4) - clone-object.ts: remove unnecessary 'as string' cast (S4325) - Update index.ts to import from new consolidated files Agent-Logs-Url: https://github.com/abapify/adt-cli/sessions/778ec8cf-fb3c-47c4-94aa-5588b60d0ed1 Co-authored-by: ThePlenkov <6381507+ThePlenkov@users.noreply.github.com> --- .../adt-mcp/src/lib/tools/call-hierarchy.ts | 186 ++++++++++++++++++ .../adt-mcp/src/lib/tools/clone-object.ts | 2 +- .../adt-mcp/src/lib/tools/function-tools.ts | 142 +++++++++++++ .../adt-mcp/src/lib/tools/get-callees-of.ts | 106 ---------- .../adt-mcp/src/lib/tools/get-callers-of.ts | 106 ---------- .../src/lib/tools/get-function-group.ts | 71 ------- .../adt-mcp/src/lib/tools/get-function.ts | 77 -------- .../src/lib/tools/get-installed-components.ts | 8 +- packages/adt-mcp/src/lib/tools/index.ts | 12 +- 9 files changed, 343 insertions(+), 367 deletions(-) create mode 100644 packages/adt-mcp/src/lib/tools/call-hierarchy.ts create mode 100644 packages/adt-mcp/src/lib/tools/function-tools.ts delete mode 100644 packages/adt-mcp/src/lib/tools/get-callees-of.ts delete mode 100644 packages/adt-mcp/src/lib/tools/get-callers-of.ts delete mode 100644 packages/adt-mcp/src/lib/tools/get-function-group.ts delete mode 100644 packages/adt-mcp/src/lib/tools/get-function.ts diff --git a/packages/adt-mcp/src/lib/tools/call-hierarchy.ts b/packages/adt-mcp/src/lib/tools/call-hierarchy.ts new file mode 100644 index 00000000..6b435628 --- /dev/null +++ b/packages/adt-mcp/src/lib/tools/call-hierarchy.ts @@ -0,0 +1,186 @@ +/** + * Tools: get_callers_of + get_callees_of – call hierarchy navigation + * + * get_callers_of: traverse the call hierarchy upward (who calls this + * method/function/subroutine). + * get_callees_of: traverse the call hierarchy downward (what does this + * method/function/subroutine call). + * + * ADT endpoints: + * GET /sap/bc/adt/repository/informationsystem/callers + * GET /sap/bc/adt/repository/informationsystem/callees + */ + +import { z } from 'zod'; +import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import type { ToolContext } from '../types'; +import { connectionShape } from './shared-schemas'; +import { resolveObjectUri } from './utils'; + +const callHierarchyShape = { + ...connectionShape, + objectName: z + .string() + .describe('Name of the ABAP object (class, function group, program)'), + objectType: z + .string() + .optional() + .describe('Object type (e.g. CLAS, FUGR, PROG)'), + objectUri: z + .string() + .optional() + .describe( + 'Direct ADT URI of the object (skips name resolution if provided)', + ), + maxResults: z + .number() + .optional() + .describe('Maximum number of results (default: 50)'), +}; + +async function fetchCallHierarchy( + client: ReturnType, + endpoint: 'callers' | 'callees', + objectName: string, + objectType: string | undefined, + objectUri: string | undefined, + maxResults: number, +): Promise<{ objectUri: string; result: unknown } | null> { + const resolvedUri = + objectUri ?? (await resolveObjectUri(client, objectName, objectType)); + if (!resolvedUri) return null; + + const params = new URLSearchParams({ + objectUri: resolvedUri, + maxResults: String(maxResults), + }); + + const result = await client.fetch( + `/sap/bc/adt/repository/informationsystem/${endpoint}?${params.toString()}`, + { method: 'GET', headers: { Accept: 'application/json' } }, + ); + + return { objectUri: resolvedUri, result }; +} + +export function registerGetCallersOfTool( + server: McpServer, + ctx: ToolContext, +): void { + server.tool( + 'get_callers_of', + 'Find all callers (upward call hierarchy) of an ABAP method, function module, or subroutine', + callHierarchyShape, + async (args) => { + try { + const client = ctx.getClient(args); + const res = await fetchCallHierarchy( + client, + 'callers', + args.objectName, + args.objectType, + args.objectUri, + args.maxResults ?? 50, + ); + if (!res) { + return { + isError: true, + content: [ + { + type: 'text' as const, + text: `Object '${args.objectName}' not found`, + }, + ], + }; + } + return { + content: [ + { + type: 'text' as const, + text: JSON.stringify( + { + objectName: args.objectName, + objectUri: res.objectUri, + callers: res.result, + }, + null, + 2, + ), + }, + ], + }; + } catch (error) { + return { + isError: true, + content: [ + { + type: 'text' as const, + text: `Get callers failed: ${error instanceof Error ? error.message : String(error)}`, + }, + ], + }; + } + }, + ); +} + +export function registerGetCalleesOfTool( + server: McpServer, + ctx: ToolContext, +): void { + server.tool( + 'get_callees_of', + 'Find all callees (downward call hierarchy) of an ABAP method, function module, or subroutine', + callHierarchyShape, + async (args) => { + try { + const client = ctx.getClient(args); + const res = await fetchCallHierarchy( + client, + 'callees', + args.objectName, + args.objectType, + args.objectUri, + args.maxResults ?? 50, + ); + if (!res) { + return { + isError: true, + content: [ + { + type: 'text' as const, + text: `Object '${args.objectName}' not found`, + }, + ], + }; + } + return { + content: [ + { + type: 'text' as const, + text: JSON.stringify( + { + objectName: args.objectName, + objectUri: res.objectUri, + callees: res.result, + }, + null, + 2, + ), + }, + ], + }; + } catch (error) { + return { + isError: true, + content: [ + { + type: 'text' as const, + text: `Get callees failed: ${error instanceof Error ? error.message : String(error)}`, + }, + ], + }; + } + }, + ); +} diff --git a/packages/adt-mcp/src/lib/tools/clone-object.ts b/packages/adt-mcp/src/lib/tools/clone-object.ts index 3dda15b0..e2aa39cc 100644 --- a/packages/adt-mcp/src/lib/tools/clone-object.ts +++ b/packages/adt-mcp/src/lib/tools/clone-object.ts @@ -201,7 +201,7 @@ export function registerCloneObjectTool( { method: 'PUT', headers: { 'Content-Type': 'text/plain' }, - body: sourceCode as string, + body: sourceCode, }, ); } finally { diff --git a/packages/adt-mcp/src/lib/tools/function-tools.ts b/packages/adt-mcp/src/lib/tools/function-tools.ts new file mode 100644 index 00000000..853821e0 --- /dev/null +++ b/packages/adt-mcp/src/lib/tools/function-tools.ts @@ -0,0 +1,142 @@ +/** + * Tools: get_function_group + get_function – ABAP function group and module access + * + * get_function_group: retrieve function group metadata and optionally source. + * get_function: retrieve function module metadata and optionally source. + * + * ADT endpoints: + * GET /sap/bc/adt/functions/groups/{groupName} + * GET /sap/bc/adt/functions/groups/{groupName}/source/main + * GET /sap/bc/adt/functions/groups/{groupName}/fmodules/{fmName} + * GET /sap/bc/adt/functions/groups/{groupName}/fmodules/{fmName}/source/main + */ + +import { z } from 'zod'; +import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import type { ToolContext } from '../types'; +import { connectionShape } from './shared-schemas'; + +function formatMetadataResult( + metadata: unknown, + source: string | undefined, +): string { + return JSON.stringify( + source === undefined ? { metadata } : { metadata, source }, + null, + 2, + ); +} + +export function registerGetFunctionGroupTool( + server: McpServer, + ctx: ToolContext, +): void { + server.tool( + 'get_function_group', + 'Read ABAP function group metadata (description, includes). Optionally includes source code.', + { + ...connectionShape, + groupName: z.string().describe('Function group name (e.g. ZFUGR_UTIL)'), + includeSource: z + .boolean() + .optional() + .describe( + 'Whether to also return the main include source code (default: false)', + ), + }, + async (args) => { + try { + const client = ctx.getClient(args); + const name = args.groupName.toLowerCase(); + + const metadata = await client.adt.functions.groups.get(name); + + let source: string | undefined; + if (args.includeSource) { + source = (await client.adt.functions.groups.source.main.get( + name, + )) as string; + } + + return { + content: [ + { + type: 'text' as const, + text: formatMetadataResult(metadata, source), + }, + ], + }; + } catch (error) { + return { + isError: true, + content: [ + { + type: 'text' as const, + text: `Get function group failed: ${error instanceof Error ? error.message : String(error)}`, + }, + ], + }; + } + }, + ); +} + +export function registerGetFunctionTool( + server: McpServer, + ctx: ToolContext, +): void { + server.tool( + 'get_function', + 'Read ABAP function module metadata (parameters, exceptions) and optionally its source code.', + { + ...connectionShape, + groupName: z.string().describe('Function group name (e.g. ZFUGR_UTIL)'), + functionName: z + .string() + .describe('Function module name (e.g. Z_MY_FUNCTION)'), + includeSource: z + .boolean() + .optional() + .describe('Whether to also return the source code (default: false)'), + }, + async (args) => { + try { + const client = ctx.getClient(args); + const groupName = args.groupName.toLowerCase(); + const fmName = args.functionName.toLowerCase(); + + const metadata = await client.adt.functions.groups.fmodules.get( + groupName, + fmName, + ); + + let source: string | undefined; + if (args.includeSource) { + source = (await client.fetch( + `/sap/bc/adt/functions/groups/${groupName}/fmodules/${fmName}/source/main`, + { method: 'GET', headers: { Accept: 'text/plain' } }, + )) as string; + } + + return { + content: [ + { + type: 'text' as const, + text: formatMetadataResult(metadata, source), + }, + ], + }; + } catch (error) { + return { + isError: true, + content: [ + { + type: 'text' as const, + text: `Get function failed: ${error instanceof Error ? error.message : String(error)}`, + }, + ], + }; + } + }, + ); +} diff --git a/packages/adt-mcp/src/lib/tools/get-callees-of.ts b/packages/adt-mcp/src/lib/tools/get-callees-of.ts deleted file mode 100644 index bbd15b59..00000000 --- a/packages/adt-mcp/src/lib/tools/get-callees-of.ts +++ /dev/null @@ -1,106 +0,0 @@ -/** - * Tool: get_callees_of – find all callees of an ABAP method or function - * - * Uses the ADT repository information system callees endpoint to traverse - * the call hierarchy downward (what does this method/function call). - * - * ADT endpoint: GET /sap/bc/adt/repository/informationsystem/callees - */ - -import { z } from 'zod'; -import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; -import type { ToolContext } from '../types'; -import { connectionShape } from './shared-schemas'; -import { resolveObjectUri } from './utils'; - -export function registerGetCalleesOfTool( - server: McpServer, - ctx: ToolContext, -): void { - server.tool( - 'get_callees_of', - 'Find all callees (downward call hierarchy) of an ABAP method, function module, or subroutine', - { - ...connectionShape, - objectName: z - .string() - .describe('Name of the ABAP object (class, function group, program)'), - objectType: z - .string() - .optional() - .describe('Object type (e.g. CLAS, FUGR, PROG)'), - objectUri: z - .string() - .optional() - .describe( - 'Direct ADT URI of the object (skips name resolution if provided)', - ), - maxResults: z - .number() - .optional() - .describe('Maximum number of results (default: 50)'), - }, - async (args) => { - try { - const client = ctx.getClient(args); - const maxResults = args.maxResults ?? 50; - - let objectUri = args.objectUri; - if (!objectUri) { - objectUri = await resolveObjectUri( - client, - args.objectName, - args.objectType, - ); - if (!objectUri) { - return { - isError: true, - content: [ - { - type: 'text' as const, - text: `Object '${args.objectName}' not found`, - }, - ], - }; - } - } - - const params = new URLSearchParams({ - objectUri, - maxResults: String(maxResults), - }); - - const result = await client.fetch( - `/sap/bc/adt/repository/informationsystem/callees?${params.toString()}`, - { - method: 'GET', - headers: { Accept: 'application/json' }, - }, - ); - - return { - content: [ - { - type: 'text' as const, - text: JSON.stringify( - { objectName: args.objectName, objectUri, callees: result }, - null, - 2, - ), - }, - ], - }; - } catch (error) { - return { - isError: true, - content: [ - { - type: 'text' as const, - text: `Get callees failed: ${error instanceof Error ? error.message : String(error)}`, - }, - ], - }; - } - }, - ); -} diff --git a/packages/adt-mcp/src/lib/tools/get-callers-of.ts b/packages/adt-mcp/src/lib/tools/get-callers-of.ts deleted file mode 100644 index 8806be3a..00000000 --- a/packages/adt-mcp/src/lib/tools/get-callers-of.ts +++ /dev/null @@ -1,106 +0,0 @@ -/** - * Tool: get_callers_of – find all callers of an ABAP method or function - * - * Uses the ADT repository information system callers endpoint to traverse - * the call hierarchy upward (who calls this method/function). - * - * ADT endpoint: GET /sap/bc/adt/repository/informationsystem/callers - */ - -import { z } from 'zod'; -import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; -import type { ToolContext } from '../types'; -import { connectionShape } from './shared-schemas'; -import { resolveObjectUri } from './utils'; - -export function registerGetCallersOfTool( - server: McpServer, - ctx: ToolContext, -): void { - server.tool( - 'get_callers_of', - 'Find all callers (upward call hierarchy) of an ABAP method, function module, or subroutine', - { - ...connectionShape, - objectName: z - .string() - .describe('Name of the ABAP object (class, function group, program)'), - objectType: z - .string() - .optional() - .describe('Object type (e.g. CLAS, FUGR, PROG)'), - objectUri: z - .string() - .optional() - .describe( - 'Direct ADT URI of the object (skips name resolution if provided)', - ), - maxResults: z - .number() - .optional() - .describe('Maximum number of results (default: 50)'), - }, - async (args) => { - try { - const client = ctx.getClient(args); - const maxResults = args.maxResults ?? 50; - - let objectUri = args.objectUri; - if (!objectUri) { - objectUri = await resolveObjectUri( - client, - args.objectName, - args.objectType, - ); - if (!objectUri) { - return { - isError: true, - content: [ - { - type: 'text' as const, - text: `Object '${args.objectName}' not found`, - }, - ], - }; - } - } - - const params = new URLSearchParams({ - objectUri, - maxResults: String(maxResults), - }); - - const result = await client.fetch( - `/sap/bc/adt/repository/informationsystem/callers?${params.toString()}`, - { - method: 'GET', - headers: { Accept: 'application/json' }, - }, - ); - - return { - content: [ - { - type: 'text' as const, - text: JSON.stringify( - { objectName: args.objectName, objectUri, callers: result }, - null, - 2, - ), - }, - ], - }; - } catch (error) { - return { - isError: true, - content: [ - { - type: 'text' as const, - text: `Get callers failed: ${error instanceof Error ? error.message : String(error)}`, - }, - ], - }; - } - }, - ); -} diff --git a/packages/adt-mcp/src/lib/tools/get-function-group.ts b/packages/adt-mcp/src/lib/tools/get-function-group.ts deleted file mode 100644 index de3aff46..00000000 --- a/packages/adt-mcp/src/lib/tools/get-function-group.ts +++ /dev/null @@ -1,71 +0,0 @@ -/** - * Tool: get_function_group – read ABAP function group metadata and source - * - * Retrieves the function group metadata (description, includes) and - * optionally the main include source code. - * - * ADT endpoint: /sap/bc/adt/functions/groups/{groupName} - */ - -import { z } from 'zod'; -import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; -import type { ToolContext } from '../types'; -import { connectionShape } from './shared-schemas'; - -export function registerGetFunctionGroupTool( - server: McpServer, - ctx: ToolContext, -): void { - server.tool( - 'get_function_group', - 'Read ABAP function group metadata (description, includes). Optionally includes source code.', - { - ...connectionShape, - groupName: z.string().describe('Function group name (e.g. ZFUGR_UTIL)'), - includeSource: z - .boolean() - .optional() - .describe( - 'Whether to also return the main include source code (default: false)', - ), - }, - async (args) => { - try { - const client = ctx.getClient(args); - const name = args.groupName.toLowerCase(); - - const metadata = await client.adt.functions.groups.get(name); - - let source: string | undefined; - if (args.includeSource) { - source = (await client.adt.functions.groups.source.main.get( - name, - )) as string; - } - - return { - content: [ - { - type: 'text' as const, - text: JSON.stringify( - { metadata, ...(source !== undefined ? { source } : {}) }, - null, - 2, - ), - }, - ], - }; - } catch (error) { - return { - isError: true, - content: [ - { - type: 'text' as const, - text: `Get function group failed: ${error instanceof Error ? error.message : String(error)}`, - }, - ], - }; - } - }, - ); -} diff --git a/packages/adt-mcp/src/lib/tools/get-function.ts b/packages/adt-mcp/src/lib/tools/get-function.ts deleted file mode 100644 index e4d0a47b..00000000 --- a/packages/adt-mcp/src/lib/tools/get-function.ts +++ /dev/null @@ -1,77 +0,0 @@ -/** - * Tool: get_function – read ABAP function module metadata and source - * - * Retrieves the function module metadata (signature, parameters, exceptions) - * and optionally the source code. - * - * ADT endpoint: /sap/bc/adt/functions/groups/{groupName}/fmodules/{fmName} - */ - -import { z } from 'zod'; -import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; -import type { ToolContext } from '../types'; -import { connectionShape } from './shared-schemas'; - -export function registerGetFunctionTool( - server: McpServer, - ctx: ToolContext, -): void { - server.tool( - 'get_function', - 'Read ABAP function module metadata (parameters, exceptions) and optionally its source code.', - { - ...connectionShape, - groupName: z.string().describe('Function group name (e.g. ZFUGR_UTIL)'), - functionName: z - .string() - .describe('Function module name (e.g. Z_MY_FUNCTION)'), - includeSource: z - .boolean() - .optional() - .describe('Whether to also return the source code (default: false)'), - }, - async (args) => { - try { - const client = ctx.getClient(args); - const groupName = args.groupName.toLowerCase(); - const fmName = args.functionName.toLowerCase(); - - const metadata = await client.adt.functions.groups.fmodules.get( - groupName, - fmName, - ); - - let source: string | undefined; - if (args.includeSource) { - source = (await client.fetch( - `/sap/bc/adt/functions/groups/${groupName}/fmodules/${fmName}/source/main`, - { method: 'GET', headers: { Accept: 'text/plain' } }, - )) as string; - } - - return { - content: [ - { - type: 'text' as const, - text: JSON.stringify( - { metadata, ...(source !== undefined ? { source } : {}) }, - null, - 2, - ), - }, - ], - }; - } catch (error) { - return { - isError: true, - content: [ - { - type: 'text' as const, - text: `Get function failed: ${error instanceof Error ? error.message : String(error)}`, - }, - ], - }; - } - }, - ); -} diff --git a/packages/adt-mcp/src/lib/tools/get-installed-components.ts b/packages/adt-mcp/src/lib/tools/get-installed-components.ts index 5a8359d3..a0f48f7e 100644 --- a/packages/adt-mcp/src/lib/tools/get-installed-components.ts +++ b/packages/adt-mcp/src/lib/tools/get-installed-components.ts @@ -7,7 +7,6 @@ * ADT endpoint: GET /sap/bc/adt/system/softwarecomponents */ -import { z } from 'zod'; import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import type { ToolContext } from '../types'; import { connectionShape } from './shared-schemas'; @@ -129,7 +128,12 @@ export function registerGetFeaturesTool( { type: 'text' as const, text: JSON.stringify( - { features, discoveryServices: [...services].sort() }, + { + features, + discoveryServices: [...services].sort((a, b) => + a.localeCompare(b), + ), + }, null, 2, ), diff --git a/packages/adt-mcp/src/lib/tools/index.ts b/packages/adt-mcp/src/lib/tools/index.ts index fdf9d3fc..13e373ac 100644 --- a/packages/adt-mcp/src/lib/tools/index.ts +++ b/packages/adt-mcp/src/lib/tools/index.ts @@ -29,14 +29,18 @@ import { registerGetTableContentsTool } from './get-table-contents'; import { registerRunQueryTool } from './run-query'; import { registerFindDefinitionTool } from './find-definition'; import { registerFindReferencesTool } from './find-references'; -import { registerGetCallersOfTool } from './get-callers-of'; -import { registerGetCalleesOfTool } from './get-callees-of'; +import { + registerGetCallersOfTool, + registerGetCalleesOfTool, +} from './call-hierarchy'; import { registerCreateObjectTool } from './create-object'; import { registerDeleteObjectTool } from './delete-object'; import { registerActivatePackageTool } from './activate-package'; // Medium-priority feature parity (#M1–#M10) -import { registerGetFunctionGroupTool } from './get-function-group'; -import { registerGetFunctionTool } from './get-function'; +import { + registerGetFunctionGroupTool, + registerGetFunctionTool, +} from './function-tools'; import { registerLockObjectTool } from './lock-object'; import { registerUnlockObjectTool } from './unlock-object'; import { registerGetObjectStructureTool } from './get-object-structure'; From ce3d19e68aff622b481e46ba8c223a34ad7d1919 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 13 Apr 2026 21:59:23 +0000 Subject: [PATCH 7/9] chore: add copilot-setup-steps to enforce pre-commit format hook The Copilot agent's core.hooksPath is set to .git/copilot-hooks/ by the infrastructure (not .husky/), so the existing husky pre-commit hook was never running for agent commits. copilot-setup-steps.yml now: - Installs bun and project dependencies (so bunx nx is available) - Creates .git/copilot-hooks/pre-commit that runs nx format:write --uncommitted + git update-index --again before every agent commit Also makes .husky/pre-commit bun-aware (bunx first, npx fallback) for consistency with human developer environments. Agent-Logs-Url: https://github.com/abapify/adt-cli/sessions/19e1267a-27da-4a8e-b4d3-b3f451494958 Co-authored-by: ThePlenkov <6381507+ThePlenkov@users.noreply.github.com> --- .github/workflows/copilot-setup-steps.yml | 44 +++++++++++++++++++++++ .husky/pre-commit | 6 +++- 2 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/copilot-setup-steps.yml diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml new file mode 100644 index 00000000..497c7e73 --- /dev/null +++ b/.github/workflows/copilot-setup-steps.yml @@ -0,0 +1,44 @@ +name: 'Copilot Setup Steps' + +# Run automatically when changed, and allow manual validation from the Actions tab. +on: + workflow_dispatch: + push: + paths: + - .github/workflows/copilot-setup-steps.yml + pull_request: + paths: + - .github/workflows/copilot-setup-steps.yml + +jobs: + # Must be named exactly `copilot-setup-steps` for Copilot to pick it up. + copilot-setup-steps: + runs-on: ubuntu-latest + permissions: + contents: read + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install bun + uses: oven-sh/setup-bun@v2 + + - name: Install dependencies + run: bun install + + - name: Install pre-commit format hook for Copilot agent + run: | + mkdir -p .git/copilot-hooks + cat > .git/copilot-hooks/pre-commit << 'EOF' + #!/bin/sh + # Auto-format all staged files before committing. + # Runs nx format:write (Prettier) and re-stages any files it changed. + if command -v bunx >/dev/null 2>&1; then + bunx nx format:write --uncommitted + else + npx nx format:write --uncommitted + fi + git update-index --again + EOF + chmod +x .git/copilot-hooks/pre-commit diff --git a/.husky/pre-commit b/.husky/pre-commit index d87790a9..9279dff7 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,3 +1,7 @@ -npx nx format:write --uncommitted +if command -v bunx >/dev/null 2>&1; then + bunx nx format:write --uncommitted +else + npx nx format:write --uncommitted +fi git update-index --again From e3dce0acd0b0ab3807aba5eceb58c699e7ba1723 Mon Sep 17 00:00:00 2001 From: Petr Plenkov Date: Tue, 14 Apr 2026 23:46:12 +0200 Subject: [PATCH 8/9] refactor: improve code formatting and remove unused tests for transport tools --- packages/adt-mcp/src/lib/mock/server.ts | 1 - .../adt-mcp/src/lib/tools/activate-package.ts | 4 +- .../adt-mcp/src/lib/tools/clone-object.ts | 32 +++++++++++---- .../adt-mcp/src/lib/tools/create-object.ts | 8 +--- .../src/lib/tools/cts-release-transport.ts | 3 +- .../adt-mcp/src/lib/tools/delete-object.ts | 14 ++++--- .../adt-mcp/src/lib/tools/find-definition.ts | 3 +- .../src/lib/tools/get-installed-components.ts | 23 +++++------ .../src/lib/tools/get-table-contents.ts | 6 +-- packages/adt-mcp/src/lib/tools/get-table.ts | 9 ++++- .../adt-mcp/src/lib/tools/grep-objects.ts | 20 +++++----- .../adt-mcp/src/lib/tools/grep-packages.ts | 4 +- packages/adt-mcp/src/lib/tools/run-query.ts | 6 ++- packages/adt-mcp/tests/integration.test.ts | 40 ------------------- 14 files changed, 74 insertions(+), 99 deletions(-) diff --git a/packages/adt-mcp/src/lib/mock/server.ts b/packages/adt-mcp/src/lib/mock/server.ts index 84450637..a168cce1 100644 --- a/packages/adt-mcp/src/lib/mock/server.ts +++ b/packages/adt-mcp/src/lib/mock/server.ts @@ -361,7 +361,6 @@ function matchRoute( if ( m === 'GET' && url.startsWith('/sap/bc/adt/functions/groups/') && - !url.includes('?') && !url.includes('/objectstructure') && !url.includes('/source/') && !url.includes('/fmodules/') diff --git a/packages/adt-mcp/src/lib/tools/activate-package.ts b/packages/adt-mcp/src/lib/tools/activate-package.ts index 104346ed..b59b5d44 100644 --- a/packages/adt-mcp/src/lib/tools/activate-package.ts +++ b/packages/adt-mcp/src/lib/tools/activate-package.ts @@ -27,9 +27,7 @@ export function registerActivatePackageTool( 'Batch-activate all inactive objects in a package. Returns the count and list of activated objects.', { ...connectionShape, - packageName: z - .string() - .describe('ABAP package name (e.g. ZPACKAGE)'), + packageName: z.string().describe('ABAP package name (e.g. ZPACKAGE)'), }, async (args) => { try { diff --git a/packages/adt-mcp/src/lib/tools/clone-object.ts b/packages/adt-mcp/src/lib/tools/clone-object.ts index e2aa39cc..ba5b81f3 100644 --- a/packages/adt-mcp/src/lib/tools/clone-object.ts +++ b/packages/adt-mcp/src/lib/tools/clone-object.ts @@ -184,15 +184,18 @@ export function registerCloneObjectTool( } const lockService = createLockService(client); - const lockHandle = await lockService.lock(resolvedTargetUri, { - transport: args.transport, - objectName: targetName, - objectType: sourceType, - }); + let lockHandleStr: string | undefined; try { + const lockResult = await lockService.lock(resolvedTargetUri, { + transport: args.transport, + objectName: targetName, + objectType: sourceType, + }); + lockHandleStr = lockResult.handle; + const putParams = new URLSearchParams({ - lockHandle: lockHandle.handle, + lockHandle: lockHandleStr, ...(args.transport ? { corrNr: args.transport } : {}), }); @@ -204,10 +207,23 @@ export function registerCloneObjectTool( body: sourceCode, }, ); - } finally { + await lockService.unlock(resolvedTargetUri, { - lockHandle: lockHandle.handle, + lockHandle: lockHandleStr, }); + lockHandleStr = undefined; + } catch (lockError) { + // Best-effort unlock on failure + if (lockHandleStr) { + try { + await lockService.unlock(resolvedTargetUri, { + lockHandle: lockHandleStr, + }); + } catch { + // ignore unlock errors in error path + } + } + throw lockError; } return { diff --git a/packages/adt-mcp/src/lib/tools/create-object.ts b/packages/adt-mcp/src/lib/tools/create-object.ts index e6006153..48259b11 100644 --- a/packages/adt-mcp/src/lib/tools/create-object.ts +++ b/packages/adt-mcp/src/lib/tools/create-object.ts @@ -27,7 +27,7 @@ export function registerCreateObjectTool( ): void { server.tool( 'create_object', - 'Create a new ABAP object. Supported types: PROG (program), CLAS (class), INTF (interface), FUGR (function group), DEVC (package)', + 'Create a new ABAP object. Supported types: PROG (program), CLAS (class), INTF (interface), FUGR (function group). For packages use create_package.', { ...connectionShape, objectName: z @@ -35,11 +35,7 @@ export function registerCreateObjectTool( .describe( 'Name of the new object (uppercase, e.g. ZCL_MY_CLASS, ZPACKAGE)', ), - objectType: z - .string() - .describe( - 'Object type: PROG, CLAS, INTF, FUGR, or DEVC', - ), + objectType: z.string().describe('Object type: PROG, CLAS, INTF, or FUGR'), description: z.string().describe('Short description of the object'), packageName: z .string() diff --git a/packages/adt-mcp/src/lib/tools/cts-release-transport.ts b/packages/adt-mcp/src/lib/tools/cts-release-transport.ts index 839c884c..9b9c40f8 100644 --- a/packages/adt-mcp/src/lib/tools/cts-release-transport.ts +++ b/packages/adt-mcp/src/lib/tools/cts-release-transport.ts @@ -33,7 +33,8 @@ export function registerCtsReleaseTransportTool( { method: 'POST', headers: { - 'Content-Type': 'application/vnd.sap.adt.transportorganizer.v1+xml', + 'Content-Type': + 'application/vnd.sap.adt.transportorganizer.v1+xml', Accept: 'application/vnd.sap.adt.transportorganizer.v1+xml', }, }, diff --git a/packages/adt-mcp/src/lib/tools/delete-object.ts b/packages/adt-mcp/src/lib/tools/delete-object.ts index c436b594..4bc07aa2 100644 --- a/packages/adt-mcp/src/lib/tools/delete-object.ts +++ b/packages/adt-mcp/src/lib/tools/delete-object.ts @@ -31,7 +31,9 @@ export function registerDeleteObjectTool( transport: z .string() .optional() - .describe('Transport request number (required for transportable objects)'), + .describe( + 'Transport request number (required for transportable objects)', + ), }, async (args) => { try { @@ -68,10 +70,12 @@ export function registerDeleteObjectTool( }; } - const params = args.transport - ? `?corrNr=${encodeURIComponent(args.transport)}` - : ''; - await client.fetch(`${uri}${params}`, { method: 'DELETE' }); + const params = new URLSearchParams(); + if (args.transport) params.set('corrNr', args.transport); + const qs = params.toString(); + await client.fetch(`${uri}${qs ? `?${qs}` : ''}`, { + method: 'DELETE', + }); } return { diff --git a/packages/adt-mcp/src/lib/tools/find-definition.ts b/packages/adt-mcp/src/lib/tools/find-definition.ts index 8454aed8..26dcae1b 100644 --- a/packages/adt-mcp/src/lib/tools/find-definition.ts +++ b/packages/adt-mcp/src/lib/tools/find-definition.ts @@ -51,8 +51,7 @@ export function registerFindDefinitionTool( }); if (args.objectType) params.set('objectType', args.objectType); - if (args.parentObjectName) - params.set('context', args.parentObjectName); + if (args.parentObjectName) params.set('context', args.parentObjectName); if (args.parentObjectType) params.set('contextType', args.parentObjectType); diff --git a/packages/adt-mcp/src/lib/tools/get-installed-components.ts b/packages/adt-mcp/src/lib/tools/get-installed-components.ts index a0f48f7e..ec45db85 100644 --- a/packages/adt-mcp/src/lib/tools/get-installed-components.ts +++ b/packages/adt-mcp/src/lib/tools/get-installed-components.ts @@ -100,27 +100,26 @@ export function registerGetFeaturesTool( } // Feature detection heuristics based on known ADT paths + const serviceList = [...services]; const features: Record = { atc: services.has('/sap/bc/adt/atc') || - [...services].some((s) => s.includes('/atc')), - cts: [...services].some((s) => s.includes('/cts')), - aunit: [...services].some( + serviceList.some((s) => s.includes('/atc')), + cts: serviceList.some((s) => s.includes('/cts')), + aunit: serviceList.some( (s) => s.includes('/abapunit') || s.includes('/aunit'), ), - abapgit: [...services].some((s) => s.includes('/abapgit')), - rap: [...services].some( + abapgit: serviceList.some((s) => s.includes('/abapgit')), + rap: serviceList.some( (s) => s.includes('/businessservices') || s.includes('/rap'), ), - ui5: [...services].some( + ui5: serviceList.some( (s) => s.includes('/ui5') || s.includes('/bsp'), ), - classicBadi: [...services].some((s) => s.includes('/enhancements')), - prettyPrinter: [...services].some((s) => - s.includes('/prettyprinter'), - ), - dataPreview: [...services].some((s) => s.includes('/datapreview')), - navigation: [...services].some((s) => s.includes('/navigation')), + classicBadi: serviceList.some((s) => s.includes('/enhancements')), + prettyPrinter: serviceList.some((s) => s.includes('/prettyprinter')), + dataPreview: serviceList.some((s) => s.includes('/datapreview')), + navigation: serviceList.some((s) => s.includes('/navigation')), }; return { diff --git a/packages/adt-mcp/src/lib/tools/get-table-contents.ts b/packages/adt-mcp/src/lib/tools/get-table-contents.ts index 32403c87..c1db0ad5 100644 --- a/packages/adt-mcp/src/lib/tools/get-table-contents.ts +++ b/packages/adt-mcp/src/lib/tools/get-table-contents.ts @@ -18,12 +18,10 @@ export function registerGetTableContentsTool( ): void { server.tool( 'get_table_contents', - 'Read data from a DDIC table with optional WHERE filter, column selection, and row limit', + 'Read data from a DDIC table with optional WHERE filter, column selection, and row limit. WARNING: the WHERE clause is sent as-is to the SAP data preview endpoint — avoid untrusted input.', { ...connectionShape, - tableName: z - .string() - .describe('DDIC table name (e.g. MARA, VBAK, T001)'), + tableName: z.string().describe('DDIC table name (e.g. MARA, VBAK, T001)'), where: z .string() .optional() diff --git a/packages/adt-mcp/src/lib/tools/get-table.ts b/packages/adt-mcp/src/lib/tools/get-table.ts index d0dafda4..6836e5d6 100644 --- a/packages/adt-mcp/src/lib/tools/get-table.ts +++ b/packages/adt-mcp/src/lib/tools/get-table.ts @@ -12,13 +12,18 @@ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import type { ToolContext } from '../types'; import { connectionShape } from './shared-schemas'; -export function registerGetTableTool(server: McpServer, ctx: ToolContext): void { +export function registerGetTableTool( + server: McpServer, + ctx: ToolContext, +): void { server.tool( 'get_table', 'Read DDIC table or structure definition (fields, keys, data elements)', { ...connectionShape, - tableName: z.string().describe('DDIC table or structure name (e.g. MARA, VBAK)'), + tableName: z + .string() + .describe('DDIC table or structure name (e.g. MARA, VBAK)'), }, async (args) => { try { diff --git a/packages/adt-mcp/src/lib/tools/grep-objects.ts b/packages/adt-mcp/src/lib/tools/grep-objects.ts index 4413c9c0..18313d05 100644 --- a/packages/adt-mcp/src/lib/tools/grep-objects.ts +++ b/packages/adt-mcp/src/lib/tools/grep-objects.ts @@ -22,9 +22,7 @@ export function registerGrepObjectsTool( 'Regex search for a pattern within ABAP object source code. Provide either a list of object URIs or name+type pairs to resolve them.', { ...connectionShape, - pattern: z - .string() - .describe('Search pattern (regex or literal string)'), + pattern: z.string().describe('Search pattern (regex or literal string)'), objectUris: z .array(z.string()) .optional() @@ -50,15 +48,15 @@ export function registerGrepObjectsTool( const client = ctx.getClient(args); const maxResults = args.maxResults ?? 50; - // Resolve URIs from name/type pairs if provided - let uris: string[] = args.objectUris ?? []; + // Resolve URIs from name/type pairs if provided (in parallel) + const uris: string[] = args.objectUris ? [...args.objectUris] : []; if (args.objects && args.objects.length > 0) { - for (const obj of args.objects) { - const uri = await resolveObjectUri( - client, - obj.objectName, - obj.objectType, - ); + const resolved = await Promise.all( + args.objects.map((obj) => + resolveObjectUri(client, obj.objectName, obj.objectType), + ), + ); + for (const uri of resolved) { if (uri) uris.push(uri); } } diff --git a/packages/adt-mcp/src/lib/tools/grep-packages.ts b/packages/adt-mcp/src/lib/tools/grep-packages.ts index 12cb8203..534af490 100644 --- a/packages/adt-mcp/src/lib/tools/grep-packages.ts +++ b/packages/adt-mcp/src/lib/tools/grep-packages.ts @@ -21,9 +21,7 @@ export function registerGrepPackagesTool( 'Regex search for a pattern across all ABAP source code within a package (and optionally its subpackages)', { ...connectionShape, - pattern: z - .string() - .describe('Search pattern (regex or literal string)'), + pattern: z.string().describe('Search pattern (regex or literal string)'), packageName: z .string() .describe('ABAP package name to search within (e.g. ZPACKAGE)'), diff --git a/packages/adt-mcp/src/lib/tools/run-query.ts b/packages/adt-mcp/src/lib/tools/run-query.ts index 42ac8bed..cdbaa53b 100644 --- a/packages/adt-mcp/src/lib/tools/run-query.ts +++ b/packages/adt-mcp/src/lib/tools/run-query.ts @@ -70,7 +70,11 @@ export function registerRunQueryTool( content: [ { type: 'text' as const, - text: JSON.stringify({ query: trimmedQuery, data: result }, null, 2), + text: JSON.stringify( + { query: trimmedQuery, data: result }, + null, + 2, + ), }, ], }; diff --git a/packages/adt-mcp/tests/integration.test.ts b/packages/adt-mcp/tests/integration.test.ts index 604690c6..9431c2a1 100644 --- a/packages/adt-mcp/tests/integration.test.ts +++ b/packages/adt-mcp/tests/integration.test.ts @@ -198,46 +198,6 @@ describe('adt-mcp integration tests', () => { }); }); - // ── cts_create_transport ────────────────────────────────────── - - describe('cts_create_transport tool', () => { - it('returns a not-yet-implemented error', async () => { - const { raw } = await callTool('cts_create_transport', { - ...connArgs(), - description: 'Test transport', - }); - const result = raw as { - isError?: boolean; - content: Array<{ type: string; text: string }>; - }; - assert.strictEqual(result.isError, true); - assert.ok( - result.content[0]?.text.includes('not yet implemented'), - 'error message should mention not yet implemented', - ); - }); - }); - - // ── cts_release_transport ───────────────────────────────────── - - describe('cts_release_transport tool', () => { - it('returns a not-yet-implemented error', async () => { - const { raw } = await callTool('cts_release_transport', { - ...connArgs(), - transport: 'DEVK900001', - }); - const result = raw as { - isError?: boolean; - content: Array<{ type: string; text: string }>; - }; - assert.strictEqual(result.isError, true); - assert.ok( - result.content[0]?.text.includes('not yet implemented'), - 'error message should mention not yet implemented', - ); - }); - }); - // ── cts_delete_transport ─────────────────────────────────────── describe('cts_delete_transport tool', () => { From a001dede88ffa57486406380fbc24181075bc863 Mon Sep 17 00:00:00 2001 From: Petr Plenkov Date: Wed, 15 Apr 2026 16:10:20 +0200 Subject: [PATCH 9/9] fix(adt-mcp): address SonarCloud blockers in PR 101 Pin the Copilot Bun setup action, reduce duplicated tool logic, and simplify Sonar-reported code paths in adt-mcp.\n\nCo-authored-by: Codex --- .github/workflows/copilot-setup-steps.yml | 2 +- .../adt-mcp/src/lib/tools/call-hierarchy.ts | 98 +++----- .../adt-mcp/src/lib/tools/clone-object.ts | 216 ++++++++---------- .../adt-mcp/src/lib/tools/create-object.ts | 91 ++------ .../src/lib/tools/cts-create-transport.ts | 28 ++- .../adt-mcp/src/lib/tools/delete-object.ts | 80 +++++-- .../adt-mcp/src/lib/tools/object-creation.ts | 102 +++++++++ 7 files changed, 336 insertions(+), 281 deletions(-) create mode 100644 packages/adt-mcp/src/lib/tools/object-creation.ts diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index 497c7e73..b323fab4 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -22,7 +22,7 @@ jobs: uses: actions/checkout@v4 - name: Install bun - uses: oven-sh/setup-bun@v2 + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 - name: Install dependencies run: bun install diff --git a/packages/adt-mcp/src/lib/tools/call-hierarchy.ts b/packages/adt-mcp/src/lib/tools/call-hierarchy.ts index 6b435628..464a4372 100644 --- a/packages/adt-mcp/src/lib/tools/call-hierarchy.ts +++ b/packages/adt-mcp/src/lib/tools/call-hierarchy.ts @@ -63,20 +63,29 @@ async function fetchCallHierarchy( return { objectUri: resolvedUri, result }; } -export function registerGetCallersOfTool( +type CallHierarchyToolConfig = { + endpoint: 'callers' | 'callees'; + failureLabel: string; + resultKey: 'callers' | 'callees'; + toolDescription: string; + toolName: 'get_callers_of' | 'get_callees_of'; +}; + +function registerCallHierarchyTool( server: McpServer, ctx: ToolContext, + config: CallHierarchyToolConfig, ): void { server.tool( - 'get_callers_of', - 'Find all callers (upward call hierarchy) of an ABAP method, function module, or subroutine', + config.toolName, + config.toolDescription, callHierarchyShape, async (args) => { try { const client = ctx.getClient(args); const res = await fetchCallHierarchy( client, - 'callers', + config.endpoint, args.objectName, args.objectType, args.objectUri, @@ -101,7 +110,7 @@ export function registerGetCallersOfTool( { objectName: args.objectName, objectUri: res.objectUri, - callers: res.result, + [config.resultKey]: res.result, }, null, 2, @@ -115,7 +124,7 @@ export function registerGetCallersOfTool( content: [ { type: 'text' as const, - text: `Get callers failed: ${error instanceof Error ? error.message : String(error)}`, + text: `${config.failureLabel}: ${error instanceof Error ? error.message : String(error)}`, }, ], }; @@ -124,63 +133,30 @@ export function registerGetCallersOfTool( ); } +export function registerGetCallersOfTool( + server: McpServer, + ctx: ToolContext, +): void { + registerCallHierarchyTool(server, ctx, { + toolName: 'get_callers_of', + toolDescription: + 'Find all callers (upward call hierarchy) of an ABAP method, function module, or subroutine', + endpoint: 'callers', + resultKey: 'callers', + failureLabel: 'Get callers failed', + }); +} + export function registerGetCalleesOfTool( server: McpServer, ctx: ToolContext, ): void { - server.tool( - 'get_callees_of', - 'Find all callees (downward call hierarchy) of an ABAP method, function module, or subroutine', - callHierarchyShape, - async (args) => { - try { - const client = ctx.getClient(args); - const res = await fetchCallHierarchy( - client, - 'callees', - args.objectName, - args.objectType, - args.objectUri, - args.maxResults ?? 50, - ); - if (!res) { - return { - isError: true, - content: [ - { - type: 'text' as const, - text: `Object '${args.objectName}' not found`, - }, - ], - }; - } - return { - content: [ - { - type: 'text' as const, - text: JSON.stringify( - { - objectName: args.objectName, - objectUri: res.objectUri, - callees: res.result, - }, - null, - 2, - ), - }, - ], - }; - } catch (error) { - return { - isError: true, - content: [ - { - type: 'text' as const, - text: `Get callees failed: ${error instanceof Error ? error.message : String(error)}`, - }, - ], - }; - } - }, - ); + registerCallHierarchyTool(server, ctx, { + toolName: 'get_callees_of', + toolDescription: + 'Find all callees (downward call hierarchy) of an ABAP method, function module, or subroutine', + endpoint: 'callees', + resultKey: 'callees', + failureLabel: 'Get callees failed', + }); } diff --git a/packages/adt-mcp/src/lib/tools/clone-object.ts b/packages/adt-mcp/src/lib/tools/clone-object.ts index ba5b81f3..edaac131 100644 --- a/packages/adt-mcp/src/lib/tools/clone-object.ts +++ b/packages/adt-mcp/src/lib/tools/clone-object.ts @@ -14,19 +14,18 @@ import { z } from 'zod'; import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { createLockService } from '@abapify/adt-locks'; import type { ToolContext } from '../types'; +import { + createAdtObject, + isSourceBackedObjectType, + SOURCE_BACKED_OBJECT_TYPES, + type SourceBackedObjectType, +} from './object-creation'; import { connectionShape } from './shared-schemas'; import { resolveObjectUri, resolveObjectUriFromType } from './utils'; -const CLONABLE_TYPES = ['PROG', 'CLAS', 'INTF'] as const; -type ClonableType = (typeof CLONABLE_TYPES)[number]; - -function isClonableType(t: string): t is ClonableType { - return CLONABLE_TYPES.includes(t.toUpperCase() as ClonableType); -} - async function getSourceCode( client: ReturnType, - objectType: ClonableType, + objectType: SourceBackedObjectType, objectName: string, ): Promise { const name = objectName.toLowerCase(); @@ -42,42 +41,80 @@ async function getSourceCode( } } -async function createNewObject( +async function resolveCloneDescription( client: ReturnType, - objectType: ClonableType, - objectName: string, - description: string, - packageName: string | undefined, - transport: string | undefined, -): Promise { - const packageRef = packageName - ? { uri: `/sap/bc/adt/packages/${packageName.toUpperCase()}` } - : undefined; - const queryOptions = transport ? { corrNr: transport } : {}; - const commonFields = { - name: objectName.toUpperCase(), - description, - language: 'EN', - masterLanguage: 'EN', - ...(packageRef ? { packageRef } : {}), - }; + sourceType: SourceBackedObjectType, + sourceName: string, + targetDescription?: string, +): Promise { + if (targetDescription) { + return targetDescription; + } - switch (objectType) { - case 'PROG': - await client.adt.programs.programs.post(queryOptions, { - abapProgram: { ...commonFields, type: 'PROG' }, - }); - break; - case 'CLAS': - await client.adt.oo.classes.post(queryOptions, { - abapClass: { ...commonFields, type: 'CLAS/OC' }, - }); - break; - case 'INTF': - await client.adt.oo.interfaces.post(queryOptions, { - abapInterface: { ...commonFields, type: 'INTF/OI' }, - }); - break; + const sourceUri = resolveObjectUriFromType(sourceType, sourceName); + if (!sourceUri) { + return `Copy of ${sourceName}`; + } + + try { + const meta = (await client.fetch(sourceUri, { + method: 'GET', + headers: { Accept: 'application/json' }, + })) as Record; + const desc = + (meta as Record>)?.abapClass + ?.description ?? + (meta as Record>)?.abapProgram + ?.description ?? + (meta as Record>)?.abapInterface + ?.description; + + return desc ? `Copy of ${String(desc)}` : `Copy of ${sourceName}`; + } catch { + return `Copy of ${sourceName}`; + } +} + +async function copySourceToClone( + client: ReturnType, + resolvedTargetUri: string, + sourceCode: string, + targetName: string, + sourceType: SourceBackedObjectType, + transport?: string, +): Promise { + const lockService = createLockService(client); + let lockHandle: string | undefined; + + try { + const lockResult = await lockService.lock(resolvedTargetUri, { + transport, + objectName: targetName, + objectType: sourceType, + }); + lockHandle = lockResult.handle; + + const putParams = new URLSearchParams({ + lockHandle, + ...(transport ? { corrNr: transport } : {}), + }); + + await client.fetch( + `${resolvedTargetUri}/source/main?${putParams.toString()}`, + { + method: 'PUT', + headers: { 'Content-Type': 'text/plain' }, + body: sourceCode, + }, + ); + } finally { + if (lockHandle) { + try { + await lockService.unlock(resolvedTargetUri, { lockHandle }); + } catch { + // ignore unlock errors in error paths + } + } } } @@ -119,54 +156,37 @@ export function registerCloneObjectTool( const sourceName = args.sourceObjectName.toUpperCase(); const targetName = args.targetObjectName.toUpperCase(); - if (!isClonableType(sourceType)) { + if (!isSourceBackedObjectType(sourceType)) { return { isError: true, content: [ { type: 'text' as const, - text: `Object type '${sourceType}' is not supported for cloning. Supported types: ${CLONABLE_TYPES.join(', ')}`, + text: `Object type '${sourceType}' is not supported for cloning. Supported types: ${SOURCE_BACKED_OBJECT_TYPES.join(', ')}`, }, ], }; } - // 1. Get source object metadata (for description) - const sourceUri = resolveObjectUriFromType(sourceType, sourceName); - let description = args.targetDescription ?? `Copy of ${sourceName}`; - if (!args.targetDescription && sourceUri) { - try { - const meta = (await client.fetch(sourceUri, { - method: 'GET', - headers: { Accept: 'application/json' }, - })) as Record; - const desc = - (meta as Record>)?.abapClass - ?.description ?? - (meta as Record>)?.abapProgram - ?.description ?? - (meta as Record>)?.abapInterface - ?.description; - if (desc) description = `Copy of ${String(desc)}`; - } catch { - // ignore metadata fetch failure, use default description - } - } + const description = await resolveCloneDescription( + client, + sourceType, + sourceName, + args.targetDescription, + ); // 2. Get source code const sourceCode = await getSourceCode(client, sourceType, sourceName); // 3. Create the target object - await createNewObject( - client, - sourceType, - targetName, + await createAdtObject(client, { + objectType: sourceType, + objectName: targetName, description, - args.targetPackage, - args.transport, - ); + packageName: args.targetPackage, + transport: args.transport, + }); - // 4. Copy the source code to the clone const resolvedTargetUri = resolveObjectUriFromType(sourceType, targetName) ?? (await resolveObjectUri(client, targetName, sourceType)); @@ -183,48 +203,14 @@ export function registerCloneObjectTool( }; } - const lockService = createLockService(client); - let lockHandleStr: string | undefined; - - try { - const lockResult = await lockService.lock(resolvedTargetUri, { - transport: args.transport, - objectName: targetName, - objectType: sourceType, - }); - lockHandleStr = lockResult.handle; - - const putParams = new URLSearchParams({ - lockHandle: lockHandleStr, - ...(args.transport ? { corrNr: args.transport } : {}), - }); - - await client.fetch( - `${resolvedTargetUri}/source/main?${putParams.toString()}`, - { - method: 'PUT', - headers: { 'Content-Type': 'text/plain' }, - body: sourceCode, - }, - ); - - await lockService.unlock(resolvedTargetUri, { - lockHandle: lockHandleStr, - }); - lockHandleStr = undefined; - } catch (lockError) { - // Best-effort unlock on failure - if (lockHandleStr) { - try { - await lockService.unlock(resolvedTargetUri, { - lockHandle: lockHandleStr, - }); - } catch { - // ignore unlock errors in error path - } - } - throw lockError; - } + await copySourceToClone( + client, + resolvedTargetUri, + sourceCode, + targetName, + sourceType, + args.transport, + ); return { content: [ diff --git a/packages/adt-mcp/src/lib/tools/create-object.ts b/packages/adt-mcp/src/lib/tools/create-object.ts index 48259b11..b35ea3ae 100644 --- a/packages/adt-mcp/src/lib/tools/create-object.ts +++ b/packages/adt-mcp/src/lib/tools/create-object.ts @@ -11,23 +11,20 @@ import { z } from 'zod'; import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import type { ToolContext } from '../types'; +import { + createAdtObject, + CREATE_OBJECT_TYPES, + isCreateObjectType, +} from './object-creation'; import { connectionShape } from './shared-schemas'; -/** Object types supported by this tool */ -const SUPPORTED_TYPES = ['PROG', 'CLAS', 'INTF', 'FUGR', 'DEVC'] as const; -type SupportedType = (typeof SUPPORTED_TYPES)[number]; - -function isSupportedType(t: string): t is SupportedType { - return SUPPORTED_TYPES.includes(t.toUpperCase() as SupportedType); -} - export function registerCreateObjectTool( server: McpServer, ctx: ToolContext, ): void { server.tool( 'create_object', - 'Create a new ABAP object. Supported types: PROG (program), CLAS (class), INTF (interface), FUGR (function group). For packages use create_package.', + 'Create a new ABAP object. Supported types: PROG (program), CLAS (class), INTF (interface), FUGR (function group), and DEVC (package).', { ...connectionShape, objectName: z @@ -35,7 +32,9 @@ export function registerCreateObjectTool( .describe( 'Name of the new object (uppercase, e.g. ZCL_MY_CLASS, ZPACKAGE)', ), - objectType: z.string().describe('Object type: PROG, CLAS, INTF, or FUGR'), + objectType: z + .string() + .describe('Object type: PROG, CLAS, INTF, FUGR, or DEVC'), description: z.string().describe('Short description of the object'), packageName: z .string() @@ -56,81 +55,25 @@ export function registerCreateObjectTool( const objectType = args.objectType.toUpperCase(); const objectName = args.objectName.toUpperCase(); - if (!isSupportedType(objectType)) { + if (!isCreateObjectType(objectType)) { return { isError: true, content: [ { type: 'text' as const, - text: `Object type '${objectType}' is not supported. Supported types: ${SUPPORTED_TYPES.join(', ')}`, + text: `Object type '${objectType}' is not supported. Supported types: ${CREATE_OBJECT_TYPES.join(', ')}`, }, ], }; } - const packageRef = args.packageName - ? { uri: `/sap/bc/adt/packages/${args.packageName.toUpperCase()}` } - : undefined; - - const queryOptions = args.transport ? { corrNr: args.transport } : {}; - - const commonFields = { - name: objectName, + await createAdtObject(client, { + objectType, + objectName, description: args.description, - language: 'EN', - masterLanguage: 'EN', - ...(packageRef ? { packageRef } : {}), - }; - - switch (objectType) { - case 'PROG': - await client.adt.programs.programs.post(queryOptions, { - abapProgram: { ...commonFields, type: 'PROG' }, - }); - break; - - case 'CLAS': - await client.adt.oo.classes.post(queryOptions, { - abapClass: { ...commonFields, type: 'CLAS/OC' }, - }); - break; - - case 'INTF': - await client.adt.oo.interfaces.post(queryOptions, { - abapInterface: { ...commonFields, type: 'INTF/OI' }, - }); - break; - - case 'FUGR': - await client.adt.functions.groups.post(queryOptions, { - abapFunctionGroup: { ...commonFields, type: 'FUGR' }, - }); - break; - - case 'DEVC': { - const pkgBody = { - package: { - name: objectName, - type: 'DEVC/K', - description: args.description, - language: 'EN', - masterLanguage: 'EN', - attributes: { packageType: 'development' }, - superPackage: {}, - extensionAlias: {}, - switch: {}, - applicationComponent: {}, - transport: {}, - translation: {}, - useAccesses: {}, - packageInterfaces: {}, - subPackages: {}, - }, - }; - await client.adt.packages.post(queryOptions, pkgBody); - break; - } - } + packageName: args.packageName, + transport: args.transport, + }); return { content: [ diff --git a/packages/adt-mcp/src/lib/tools/cts-create-transport.ts b/packages/adt-mcp/src/lib/tools/cts-create-transport.ts index a4ba32fb..10d0014e 100644 --- a/packages/adt-mcp/src/lib/tools/cts-create-transport.ts +++ b/packages/adt-mcp/src/lib/tools/cts-create-transport.ts @@ -16,6 +16,20 @@ import { transportmanagmentCreate } from '@abapify/adt-schemas'; type CreateBody = InferTypedSchema; +function getRecord(value: unknown): Record | undefined { + return value && typeof value === 'object' + ? (value as Record) + : undefined; +} + +function getStringField( + value: Record | undefined, + key: string, +): string | undefined { + const rawValue = value?.[key]; + return typeof rawValue === 'string' ? rawValue : undefined; +} + export function registerCtsCreateTransportTool( server: McpServer, ctx: ToolContext, @@ -53,14 +67,14 @@ export function registerCtsCreateTransportTool( const response = await client.adt.cts.transportrequests.create(body); // Extract transport number from response - const data = response as Record; + const data = getRecord(response); const request = - (data.root as Record)?.request ?? - data.request ?? + getRecord(getRecord(data?.root)?.request) ?? + getRecord(data?.request) ?? data; - const trkorr = - (request as Record)?.trkorr ?? - (request as Record)?.number ?? + const transportNumber = + getStringField(request, 'trkorr') ?? + getStringField(request, 'number') ?? ''; return { @@ -70,7 +84,7 @@ export function registerCtsCreateTransportTool( text: JSON.stringify( { status: 'created', - transport: String(trkorr), + transport: transportNumber, description: args.description, type: args.type ?? 'K', }, diff --git a/packages/adt-mcp/src/lib/tools/delete-object.ts b/packages/adt-mcp/src/lib/tools/delete-object.ts index 4bc07aa2..aec3c802 100644 --- a/packages/adt-mcp/src/lib/tools/delete-object.ts +++ b/packages/adt-mcp/src/lib/tools/delete-object.ts @@ -14,6 +14,53 @@ import type { ToolContext } from '../types'; import { connectionShape } from './shared-schemas'; import { resolveObjectUri } from './utils'; +type AdtClient = ReturnType; +type QueryOptions = { corrNr?: string }; +type DeleteOperation = ( + client: AdtClient, + objectName: string, + queryOptions: QueryOptions, +) => Promise; + +const deleteOperations: Record = { + PROG: (client, objectName, queryOptions) => + client.adt.programs.programs.delete(objectName.toLowerCase(), queryOptions), + CLAS: (client, objectName, queryOptions) => + client.adt.oo.classes.delete(objectName.toLowerCase(), queryOptions), + INTF: (client, objectName, queryOptions) => + client.adt.oo.interfaces.delete(objectName.toLowerCase(), queryOptions), + FUGR: (client, objectName, queryOptions) => + client.adt.functions.groups.delete(objectName.toLowerCase(), queryOptions), + DEVC: (client, objectName, queryOptions) => + client.adt.packages.delete(objectName, queryOptions), +}; + +function getDeleteOperation(objectType?: string): DeleteOperation | undefined { + return objectType ? deleteOperations[objectType] : undefined; +} + +async function deleteByResolvedUri( + client: AdtClient, + objectName: string, + objectType: string | undefined, + transport: string | undefined, +): Promise { + const uri = await resolveObjectUri(client, objectName, objectType); + if (!uri) { + return false; + } + + const params = new URLSearchParams(); + if (transport) { + params.set('corrNr', transport); + } + + const queryString = params.toString(); + const deleteUri = queryString ? `${uri}?${queryString}` : uri; + await client.fetch(deleteUri, { method: 'DELETE' }); + return true; +} + export function registerDeleteObjectTool( server: McpServer, ctx: ToolContext, @@ -41,24 +88,18 @@ export function registerDeleteObjectTool( const objectName = args.objectName.toUpperCase(); const objectType = args.objectType?.toUpperCase(); const queryOptions = args.transport ? { corrNr: args.transport } : {}; - const nameLower = objectName.toLowerCase(); - // Use typed CRUD contracts for known types - if (objectType === 'PROG') { - await client.adt.programs.programs.delete(nameLower, queryOptions); - } else if (objectType === 'CLAS') { - await client.adt.oo.classes.delete(nameLower, queryOptions); - } else if (objectType === 'INTF') { - await client.adt.oo.interfaces.delete(nameLower, queryOptions); - } else if (objectType === 'FUGR') { - await client.adt.functions.groups.delete(nameLower, queryOptions); - } else if (objectType === 'DEVC') { - // Packages contract uses case-sensitive names - await client.adt.packages.delete(objectName, queryOptions); + const deleteOperation = getDeleteOperation(objectType); + if (deleteOperation) { + await deleteOperation(client, objectName, queryOptions); } else { - // Fall back to resolving the URI and issuing a raw DELETE - const uri = await resolveObjectUri(client, objectName, objectType); - if (!uri) { + const deleted = await deleteByResolvedUri( + client, + objectName, + objectType, + args.transport, + ); + if (!deleted) { return { isError: true, content: [ @@ -69,13 +110,6 @@ export function registerDeleteObjectTool( ], }; } - - const params = new URLSearchParams(); - if (args.transport) params.set('corrNr', args.transport); - const qs = params.toString(); - await client.fetch(`${uri}${qs ? `?${qs}` : ''}`, { - method: 'DELETE', - }); } return { diff --git a/packages/adt-mcp/src/lib/tools/object-creation.ts b/packages/adt-mcp/src/lib/tools/object-creation.ts new file mode 100644 index 00000000..a844880e --- /dev/null +++ b/packages/adt-mcp/src/lib/tools/object-creation.ts @@ -0,0 +1,102 @@ +import type { ToolContext } from '../types'; + +export const CREATE_OBJECT_TYPES = [ + 'PROG', + 'CLAS', + 'INTF', + 'FUGR', + 'DEVC', +] as const; + +export const SOURCE_BACKED_OBJECT_TYPES = ['PROG', 'CLAS', 'INTF'] as const; + +export type CreateObjectType = (typeof CREATE_OBJECT_TYPES)[number]; +export type SourceBackedObjectType = + (typeof SOURCE_BACKED_OBJECT_TYPES)[number]; + +type AdtClient = ReturnType; + +type CreateObjectArgs = { + objectType: CreateObjectType; + objectName: string; + description: string; + packageName?: string; + transport?: string; +}; + +export function isCreateObjectType(type: string): type is CreateObjectType { + return CREATE_OBJECT_TYPES.includes(type.toUpperCase() as CreateObjectType); +} + +export function isSourceBackedObjectType( + type: string, +): type is SourceBackedObjectType { + return SOURCE_BACKED_OBJECT_TYPES.includes( + type.toUpperCase() as SourceBackedObjectType, + ); +} + +export async function createAdtObject( + client: AdtClient, + args: CreateObjectArgs, +): Promise { + const packageRef = args.packageName + ? { uri: `/sap/bc/adt/packages/${args.packageName.toUpperCase()}` } + : undefined; + const queryOptions = args.transport ? { corrNr: args.transport } : {}; + const commonFields = { + name: args.objectName.toUpperCase(), + description: args.description, + language: 'EN', + masterLanguage: 'EN', + ...(packageRef ? { packageRef } : {}), + }; + + switch (args.objectType) { + case 'PROG': + await client.adt.programs.programs.post(queryOptions, { + abapProgram: { ...commonFields, type: 'PROG' }, + }); + return; + + case 'CLAS': + await client.adt.oo.classes.post(queryOptions, { + abapClass: { ...commonFields, type: 'CLAS/OC' }, + }); + return; + + case 'INTF': + await client.adt.oo.interfaces.post(queryOptions, { + abapInterface: { ...commonFields, type: 'INTF/OI' }, + }); + return; + + case 'FUGR': + await client.adt.functions.groups.post(queryOptions, { + abapFunctionGroup: { ...commonFields, type: 'FUGR' }, + }); + return; + + case 'DEVC': + await client.adt.packages.post(queryOptions, { + package: { + name: args.objectName.toUpperCase(), + type: 'DEVC/K', + description: args.description, + language: 'EN', + masterLanguage: 'EN', + attributes: { packageType: 'development' }, + superPackage: {}, + extensionAlias: {}, + switch: {}, + applicationComponent: {}, + transport: {}, + translation: {}, + useAccesses: {}, + packageInterfaces: {}, + subPackages: {}, + }, + }); + return; + } +}