sap-adt-mcp 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,107 @@
1
+ import { escapeXml } from "../xml.js";
2
+ import { errorResult, jsonResult, textResult } from "../result.js";
3
+ import { SYSTEM_HINT } from "./_shared.js";
4
+
5
+ // Background-job (SM36/SM37) and spool (SP01) integration over ADT.
6
+ //
7
+ // IMPORTANT: standard ABAP systems do NOT expose background-job scheduling or
8
+ // spool reading through ADT REST — there is no registered ADT collection for
9
+ // either (verified against on-prem NetWeaver). These tools attempt a plausible
10
+ // endpoint and degrade gracefully (available:false + hint) so an agent can fall
11
+ // back to SM36/SM37/SP01. Treat them as experimental; positive paths are
12
+ // unverified and depend on a custom/extension ADT service being installed.
13
+
14
+ const JOBS_PATH = "/sap/bc/adt/scheduling/jobs";
15
+ const SPOOL_PATH = "/sap/bc/adt/scheduling/spools";
16
+
17
+ function isResourceNotFound(status, body) {
18
+ return status === 404 || (typeof body === "string" && /ExceptionResourceNotFound/.test(body));
19
+ }
20
+
21
+ function unavailable(sys, feature, status, body) {
22
+ return jsonResult({
23
+ system: sys,
24
+ available: false,
25
+ hint:
26
+ `${feature} is not exposed via ADT REST on this system. ` +
27
+ "Use SM36/SM37 (jobs) or SP01 (spool) in the SAP GUI. " +
28
+ "No standardized ADT background-processing API exists on classic NetWeaver.",
29
+ status,
30
+ raw: typeof body === "string" ? body.slice(0, 400) : undefined,
31
+ });
32
+ }
33
+
34
+ export const tools = [
35
+ {
36
+ name: "adt_schedule_job",
37
+ description:
38
+ "Schedule an ABAP background job (SM36 analog) that runs a report with a variant. WRITE operation — subject to read-only mode. EXPERIMENTAL: no standardized ADT job-scheduling API exists on classic NetWeaver; on systems without an extension service the response carries available:false. Prefer SM36 for anything important.",
39
+ inputSchema: {
40
+ type: "object",
41
+ properties: {
42
+ system: { type: "string", description: SYSTEM_HINT },
43
+ jobName: { type: "string", description: "Background job name." },
44
+ program: { type: "string", description: "ABAP report/program to run." },
45
+ variant: { type: "string", description: "Optional report variant." },
46
+ startImmediately: {
47
+ type: "boolean",
48
+ description: "Start as soon as a work process is free (default true).",
49
+ },
50
+ },
51
+ required: ["jobName", "program"],
52
+ },
53
+ },
54
+ {
55
+ name: "adt_read_spool",
56
+ description:
57
+ "Read the output (spool list) of a background job / spool request (SP01 analog). EXPERIMENTAL — see adt_schedule_job for availability constraints; returns available:false on systems without an ADT spool service.",
58
+ inputSchema: {
59
+ type: "object",
60
+ properties: {
61
+ system: { type: "string", description: SYSTEM_HINT },
62
+ spoolId: { type: "string", description: "Spool request number." },
63
+ },
64
+ required: ["spoolId"],
65
+ },
66
+ },
67
+ ];
68
+
69
+ export function register({ getClient }) {
70
+ return {
71
+ adt_schedule_job: async (args) => {
72
+ const { client, name: sys } = getClient(args.system);
73
+ if (!args.jobName || !args.program) {
74
+ return textResult("adt_schedule_job: `jobName` and `program` are required.", true);
75
+ }
76
+ const startImmediately = args.startImmediately !== false;
77
+ const body =
78
+ `<?xml version="1.0" encoding="UTF-8"?>` +
79
+ `<job:job xmlns:job="http://www.sap.com/adt/scheduling" job:name="${escapeXml(args.jobName)}" ` +
80
+ `job:program="${escapeXml(args.program)}"` +
81
+ (args.variant ? ` job:variant="${escapeXml(args.variant)}"` : "") +
82
+ ` job:startImmediately="${startImmediately}"/>`;
83
+ const res = await client.request({
84
+ method: "POST",
85
+ path: JOBS_PATH,
86
+ headers: { "Content-Type": "application/xml" },
87
+ body,
88
+ });
89
+ const text = await res.text();
90
+ if (isResourceNotFound(res.status, text)) return unavailable(sys, "Background-job scheduling", res.status, text);
91
+ if (!res.ok) return errorResult(sys, res.status, text, res.headers.get("content-type"), { stage: "schedule" });
92
+ return jsonResult({ system: sys, jobName: args.jobName, status: "scheduled", result: text });
93
+ },
94
+
95
+ adt_read_spool: async (args) => {
96
+ const { client, name: sys } = getClient(args.system);
97
+ const res = await client.request({
98
+ path: `${SPOOL_PATH}/${encodeURIComponent(String(args.spoolId))}`,
99
+ accept: "text/plain",
100
+ });
101
+ const text = await res.text();
102
+ if (isResourceNotFound(res.status, text)) return unavailable(sys, "Spool reading", res.status, text);
103
+ if (!res.ok) return errorResult(sys, res.status, text, res.headers.get("content-type"));
104
+ return jsonResult({ system: sys, spoolId: args.spoolId, available: true, content: text });
105
+ },
106
+ };
107
+ }
@@ -0,0 +1,314 @@
1
+ import { objectUri, normalizeType } from "../object-uris.js";
2
+ import { escapeXml } from "../xml.js";
3
+ import { acquireLock, releaseLock } from "../lock.js";
4
+ import { buildCreateRequest } from "../object-create.js";
5
+ import { errorResult, jsonResult, textResult } from "../result.js";
6
+ import { OBJECT_TYPE_HINT, SYSTEM_HINT } from "./_shared.js";
7
+
8
+ export const tools = [
9
+ {
10
+ name: "adt_activate",
11
+ description:
12
+ "Activate one or more ABAP objects. In multi-developer scenarios where the object's components are locked in a separate task under the same transport, set processRedoneOOSourceVersionOnly=true to ask SAP to re-activate only the redone OO source versions (bypasses the 'Object components locked in request and separate task' 403).",
13
+ inputSchema: {
14
+ type: "object",
15
+ properties: {
16
+ system: { type: "string", description: SYSTEM_HINT },
17
+ objects: {
18
+ type: "array",
19
+ description: "Objects to activate.",
20
+ items: {
21
+ type: "object",
22
+ properties: {
23
+ name: { type: "string" },
24
+ type: { type: "string", description: OBJECT_TYPE_HINT },
25
+ group: { type: "string" },
26
+ },
27
+ required: ["name", "type"],
28
+ },
29
+ },
30
+ processRedoneOOSourceVersionOnly: {
31
+ type: "boolean",
32
+ description:
33
+ "Forward as isProcessRedoneOOSourceVerOnly=true on the ADT activate URL. Use to recover from CTS 'Object components locked in request and separate task' 403s.",
34
+ },
35
+ preauditRequested: {
36
+ type: "boolean",
37
+ description:
38
+ "Override the preauditRequested query parameter. Defaults to true.",
39
+ },
40
+ },
41
+ required: ["objects"],
42
+ },
43
+ },
44
+ {
45
+ name: "adt_create_object",
46
+ description:
47
+ "Create a new ABAP object in a package. Supported types: program, class, interface, include, functiongroup, function, cds, accesscontrol, metadataext, behaviordef, messageclass. Refused under read-only mode.",
48
+ inputSchema: {
49
+ type: "object",
50
+ properties: {
51
+ system: { type: "string", description: SYSTEM_HINT },
52
+ name: { type: "string", description: "New object name (Z*/Y* or namespace)." },
53
+ type: { type: "string", description: OBJECT_TYPE_HINT },
54
+ package: { type: "string", description: "Target package." },
55
+ description: { type: "string", description: "Short description." },
56
+ group: {
57
+ type: "string",
58
+ description: "Function group (required when creating a function module).",
59
+ },
60
+ programType: {
61
+ type: "string",
62
+ description: "Program subtype, e.g. 'executableProgram' or 'modulePool'. Default: executableProgram.",
63
+ },
64
+ responsible: {
65
+ type: "string",
66
+ description: "Responsible user. Defaults to the profile's logon user.",
67
+ },
68
+ transport: {
69
+ type: "string",
70
+ description: "Transport request ID to assign the new object to.",
71
+ },
72
+ },
73
+ required: ["name", "type", "package", "description"],
74
+ },
75
+ },
76
+ {
77
+ name: "adt_delete_object",
78
+ description: "Delete an ABAP object. Acquires a lock and issues DELETE. Refused under read-only mode.",
79
+ inputSchema: {
80
+ type: "object",
81
+ properties: {
82
+ system: { type: "string", description: SYSTEM_HINT },
83
+ object: { type: "string", description: "Object name." },
84
+ type: { type: "string", description: OBJECT_TYPE_HINT },
85
+ group: { type: "string", description: "Function group (for FUGR/FF or FUGR/I)." },
86
+ transport: { type: "string", description: "Transport request ID (corrNr)." },
87
+ },
88
+ required: ["object", "type"],
89
+ },
90
+ },
91
+ {
92
+ name: "adt_lock",
93
+ description:
94
+ "Acquire a stateful lock on an ABAP object. Returns a lockHandle to reuse across multiple adt_set_source calls. On CTS conflicts the error includes longText and t100 details (which TR is blocking, who owns it) so the caller can recover.",
95
+ inputSchema: {
96
+ type: "object",
97
+ properties: {
98
+ system: { type: "string", description: SYSTEM_HINT },
99
+ object: { type: "string", description: "Object name." },
100
+ type: { type: "string", description: OBJECT_TYPE_HINT },
101
+ group: { type: "string", description: "Function group (for FUGR/FF or FUGR/I)." },
102
+ accessMode: {
103
+ type: "string",
104
+ description: "Lock access mode. Default: MODIFY.",
105
+ },
106
+ transport: {
107
+ type: "string",
108
+ description:
109
+ "Transport request ID (sent as corrNr on the LOCK call). Use to scope the lock to a specific TR when the object is locked in another task under the same TR.",
110
+ },
111
+ },
112
+ required: ["object", "type"],
113
+ },
114
+ },
115
+ {
116
+ name: "adt_unlock",
117
+ description: "Release a stateful lock previously acquired with adt_lock.",
118
+ inputSchema: {
119
+ type: "object",
120
+ properties: {
121
+ system: { type: "string", description: SYSTEM_HINT },
122
+ object: { type: "string", description: "Object name." },
123
+ type: { type: "string", description: OBJECT_TYPE_HINT },
124
+ group: { type: "string", description: "Function group (for FUGR/FF or FUGR/I)." },
125
+ lockHandle: { type: "string", description: "The lockHandle returned by adt_lock." },
126
+ },
127
+ required: ["object", "type", "lockHandle"],
128
+ },
129
+ },
130
+ ];
131
+
132
+ export function register({ getClient }) {
133
+ return {
134
+ adt_activate: async (args) => {
135
+ if (!Array.isArray(args.objects) || args.objects.length === 0) {
136
+ return textResult(
137
+ "adt_activate: `objects` is required — non-empty array of { name, type[, group] }. " +
138
+ "Did you pass singular `objectName`/`objectType`? Wrap them: " +
139
+ "`objects: [{ name: '…', type: '…' }]`.",
140
+ true
141
+ );
142
+ }
143
+ for (let i = 0; i < args.objects.length; i++) {
144
+ const o = args.objects[i];
145
+ if (!o || typeof o.name !== "string" || typeof o.type !== "string") {
146
+ return textResult(
147
+ `adt_activate: objects[${i}] must be { name: string, type: string }.`,
148
+ true
149
+ );
150
+ }
151
+ }
152
+ const { client, name: sys } = getClient(args.system);
153
+ const refs = args.objects
154
+ .map((o) => {
155
+ const uri = objectUri({ type: o.type, name: o.name, group: o.group });
156
+ return `<adtcore:objectReference adtcore:uri="${escapeXml(uri)}" adtcore:name="${escapeXml(o.name.toUpperCase())}"/>`;
157
+ })
158
+ .join("");
159
+ const body =
160
+ `<?xml version="1.0" encoding="UTF-8"?>` +
161
+ `<adtcore:objectReferences xmlns:adtcore="http://www.sap.com/adt/core">${refs}</adtcore:objectReferences>`;
162
+ const preaudit =
163
+ typeof args.preauditRequested === "boolean"
164
+ ? args.preauditRequested
165
+ ? "true"
166
+ : "false"
167
+ : "true";
168
+ const query = { method: "activate", preauditRequested: preaudit };
169
+ if (args.processRedoneOOSourceVersionOnly) {
170
+ query.isProcessRedoneOOSourceVerOnly = "true";
171
+ }
172
+ const res = await client.request({
173
+ method: "POST",
174
+ path: "/sap/bc/adt/activation",
175
+ query,
176
+ headers: { "Content-Type": "application/xml" },
177
+ body,
178
+ });
179
+ const text = await res.text();
180
+ if (!res.ok) return errorResult(sys, res.status, text, res.headers.get("content-type"));
181
+ return jsonResult({ system: sys, status: res.status, result: text });
182
+ },
183
+
184
+ adt_create_object: async (args) => {
185
+ const { client, name: sys, profile } = getClient(args.system);
186
+ let req;
187
+ try {
188
+ req = buildCreateRequest({
189
+ type: args.type,
190
+ name: args.name,
191
+ package: args.package,
192
+ description: args.description,
193
+ group: args.group,
194
+ programType: args.programType,
195
+ responsible: args.responsible ?? profile.user,
196
+ });
197
+ } catch (err) {
198
+ return textResult(`Error: ${err.message}`, true);
199
+ }
200
+ const query = {};
201
+ if (args.transport) query.corrNr = args.transport;
202
+ const res = await client.request({
203
+ method: "POST",
204
+ path: req.path,
205
+ query,
206
+ headers: { "Content-Type": req.contentType },
207
+ body: req.body,
208
+ });
209
+ const text = await res.text();
210
+ if (!res.ok) return errorResult(sys, res.status, text, res.headers.get("content-type"));
211
+ const newUri = objectUri({
212
+ type: args.type,
213
+ name: args.name,
214
+ group: args.group,
215
+ });
216
+ return jsonResult({
217
+ system: sys,
218
+ name: args.name.toUpperCase(),
219
+ type: normalizeType(args.type),
220
+ package: args.package.toUpperCase(),
221
+ objectUri: newUri,
222
+ status: "created",
223
+ httpStatus: res.status,
224
+ });
225
+ },
226
+
227
+ adt_delete_object: async (args) => {
228
+ const { client, name: sys } = getClient(args.system);
229
+ const objUri = objectUri({
230
+ type: args.type,
231
+ name: args.object,
232
+ group: args.group,
233
+ });
234
+ const lock = await acquireLock(client, objUri, { corrNr: args.transport });
235
+ if (!lock.ok) {
236
+ return errorResult(sys, lock.status, lock.body, lock.contentType, {
237
+ stage: "lock",
238
+ });
239
+ }
240
+ const query = { lockHandle: lock.handle };
241
+ if (args.transport) query.corrNr = args.transport;
242
+ const res = await client.request({
243
+ method: "DELETE",
244
+ path: objUri,
245
+ query,
246
+ headers: { "X-sap-adt-sessiontype": "stateful" },
247
+ });
248
+ const text = await res.text();
249
+ if (!res.ok) {
250
+ try {
251
+ await releaseLock(client, objUri, lock.handle);
252
+ } catch {
253
+ // ignore
254
+ }
255
+ return errorResult(sys, res.status, text, res.headers.get("content-type"), {
256
+ stage: "delete",
257
+ });
258
+ }
259
+ return jsonResult({
260
+ system: sys,
261
+ object: args.object,
262
+ type: normalizeType(args.type),
263
+ status: "deleted",
264
+ httpStatus: res.status,
265
+ });
266
+ },
267
+
268
+ adt_lock: async (args) => {
269
+ const { client, name: sys } = getClient(args.system);
270
+ const objUri = objectUri({
271
+ type: args.type,
272
+ name: args.object,
273
+ group: args.group,
274
+ });
275
+ const lock = await acquireLock(client, objUri, {
276
+ accessMode: args.accessMode ?? "MODIFY",
277
+ corrNr: args.transport,
278
+ });
279
+ if (!lock.ok) {
280
+ return errorResult(sys, lock.status, lock.body, lock.contentType, {
281
+ stage: "lock",
282
+ });
283
+ }
284
+ return jsonResult({
285
+ system: sys,
286
+ object: args.object,
287
+ type: normalizeType(args.type),
288
+ objectUri: objUri,
289
+ lockHandle: lock.handle,
290
+ accessMode: args.accessMode ?? "MODIFY",
291
+ ...(args.transport ? { transport: args.transport } : {}),
292
+ });
293
+ },
294
+
295
+ adt_unlock: async (args) => {
296
+ const { client, name: sys } = getClient(args.system);
297
+ const objUri = objectUri({
298
+ type: args.type,
299
+ name: args.object,
300
+ group: args.group,
301
+ });
302
+ const res = await releaseLock(client, objUri, args.lockHandle);
303
+ const text = await res.text();
304
+ if (!res.ok) return errorResult(sys, res.status, text, res.headers.get("content-type"));
305
+ return jsonResult({
306
+ system: sys,
307
+ object: args.object,
308
+ type: normalizeType(args.type),
309
+ status: "unlocked",
310
+ httpStatus: res.status,
311
+ });
312
+ },
313
+ };
314
+ }
@@ -0,0 +1,147 @@
1
+ import { errorResult, jsonResult, textResult } from "../result.js";
2
+ import { SYSTEM_HINT } from "./_shared.js";
3
+
4
+ // SAP Note (SNOTE / Note Assistant) integration over ADT.
5
+ //
6
+ // IMPORTANT: a stable SAP Note ADT REST API is only present on systems that ship
7
+ // the Note Assistant ADT plug-in (modern S/4HANA). Classic NetWeaver stacks do
8
+ // NOT expose it — every call returns ExceptionResourceNotFound. These tools
9
+ // detect that and respond with available:false + a hint rather than a raw 404,
10
+ // so an agent can fall back to GUI SNOTE. The endpoint shape used here
11
+ // (/sap/bc/adt/cwb/notes/{id}) is the documented Note Assistant path; treat
12
+ // these tools as experimental until verified against an S/4 system.
13
+
14
+ const NOTES_BASE = "/sap/bc/adt/cwb/notes";
15
+
16
+ function normalizeNoteId(raw) {
17
+ const s = String(raw).trim();
18
+ // SAP note numbers are numeric; ADT expects the zero-padded canonical form.
19
+ if (/^\d+$/.test(s)) return s.padStart(10, "0");
20
+ return s;
21
+ }
22
+
23
+ function isResourceNotFound(status, body) {
24
+ return (
25
+ status === 404 ||
26
+ (typeof body === "string" && /ExceptionResourceNotFound/.test(body))
27
+ );
28
+ }
29
+
30
+ function unavailable(sys, noteId, status, body) {
31
+ return jsonResult({
32
+ system: sys,
33
+ note: noteId,
34
+ available: false,
35
+ hint:
36
+ "The SAP Note ADT API (Note Assistant) is not available on this system. " +
37
+ "It ships with modern S/4HANA only; classic NetWeaver has no SNOTE REST endpoint. " +
38
+ "Use transaction SNOTE in the SAP GUI instead.",
39
+ status,
40
+ raw: typeof body === "string" ? body.slice(0, 600) : undefined,
41
+ });
42
+ }
43
+
44
+ export const tools = [
45
+ {
46
+ name: "adt_get_note",
47
+ description:
48
+ "Fetch SAP Note metadata (title, component, version, type, implementation prerequisites) via the Note Assistant ADT API. EXPERIMENTAL — only available on systems shipping the SNOTE ADT plug-in (modern S/4HANA). On systems without it the response carries available:false and a hint to use GUI SNOTE.",
49
+ inputSchema: {
50
+ type: "object",
51
+ properties: {
52
+ system: { type: "string", description: SYSTEM_HINT },
53
+ note: { type: "string", description: "SAP Note number (e.g. '3076322'). Zero-padding is applied automatically." },
54
+ },
55
+ required: ["note"],
56
+ },
57
+ },
58
+ {
59
+ name: "adt_check_note_status",
60
+ description:
61
+ "Report the implementation status of a SAP Note in the system (e.g. can be implemented / obsolete / fully implemented / partially implemented). EXPERIMENTAL — see adt_get_note for availability constraints.",
62
+ inputSchema: {
63
+ type: "object",
64
+ properties: {
65
+ system: { type: "string", description: SYSTEM_HINT },
66
+ note: { type: "string", description: "SAP Note number." },
67
+ },
68
+ required: ["note"],
69
+ },
70
+ },
71
+ {
72
+ name: "adt_implement_note",
73
+ description:
74
+ "Implement (download + apply) a SAP Note via the Note Assistant ADT API. WRITE operation — subject to read-only mode and requires a transport. EXPERIMENTAL and potentially disruptive: implementing a note changes system objects. Only available on systems shipping the SNOTE ADT plug-in. Prefer GUI SNOTE for anything non-trivial.",
75
+ inputSchema: {
76
+ type: "object",
77
+ properties: {
78
+ system: { type: "string", description: SYSTEM_HINT },
79
+ note: { type: "string", description: "SAP Note number to implement." },
80
+ transport: { type: "string", description: "Transport request ID to record the implementation under." },
81
+ },
82
+ required: ["note"],
83
+ },
84
+ },
85
+ ];
86
+
87
+ export function register({ getClient }) {
88
+ return {
89
+ adt_get_note: async (args) => {
90
+ const { client, name: sys } = getClient(args.system);
91
+ const noteId = normalizeNoteId(args.note);
92
+ const res = await client.request({
93
+ path: `${NOTES_BASE}/${encodeURIComponent(noteId)}`,
94
+ accept: "application/xml",
95
+ });
96
+ const text = await res.text();
97
+ if (isResourceNotFound(res.status, text)) return unavailable(sys, noteId, res.status, text);
98
+ if (!res.ok) return errorResult(sys, res.status, text, res.headers.get("content-type"));
99
+ return jsonResult({ system: sys, note: noteId, available: true, result: text });
100
+ },
101
+
102
+ adt_check_note_status: async (args) => {
103
+ const { client, name: sys } = getClient(args.system);
104
+ const noteId = normalizeNoteId(args.note);
105
+ const res = await client.request({
106
+ path: `${NOTES_BASE}/${encodeURIComponent(noteId)}`,
107
+ accept: "application/xml",
108
+ });
109
+ const text = await res.text();
110
+ if (isResourceNotFound(res.status, text)) return unavailable(sys, noteId, res.status, text);
111
+ if (!res.ok) return errorResult(sys, res.status, text, res.headers.get("content-type"));
112
+ // Surface the implementation status attribute if the payload exposes one.
113
+ const m = text.match(/(?:implementationStatus|status)="([^"]+)"/i);
114
+ return jsonResult({
115
+ system: sys,
116
+ note: noteId,
117
+ available: true,
118
+ status: m ? m[1] : null,
119
+ raw: m ? undefined : text.slice(0, 2000),
120
+ });
121
+ },
122
+
123
+ adt_implement_note: async (args) => {
124
+ const { client, name: sys } = getClient(args.system);
125
+ const noteId = normalizeNoteId(args.note);
126
+ if (!args.transport) {
127
+ return textResult(
128
+ "adt_implement_note: `transport` is required — implementing a note records object changes under a transport request.",
129
+ true,
130
+ );
131
+ }
132
+ const body =
133
+ `<?xml version="1.0" encoding="UTF-8"?>` +
134
+ `<note:implementation xmlns:note="http://www.sap.com/adt/cwb/notes" note:id="${noteId}" note:corrNr="${args.transport}"/>`;
135
+ const res = await client.request({
136
+ method: "POST",
137
+ path: `${NOTES_BASE}/${encodeURIComponent(noteId)}/deployments`,
138
+ headers: { "Content-Type": "application/xml" },
139
+ body,
140
+ });
141
+ const text = await res.text();
142
+ if (isResourceNotFound(res.status, text)) return unavailable(sys, noteId, res.status, text);
143
+ if (!res.ok) return errorResult(sys, res.status, text, res.headers.get("content-type"), { stage: "implement" });
144
+ return jsonResult({ system: sys, note: noteId, status: "implemented", transport: args.transport, result: text });
145
+ },
146
+ };
147
+ }