sap-adt-mcp 0.8.49 → 0.8.50
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -5
- package/package.json +2 -2
- package/src/adt-client.js +12 -2
- package/src/object-create.js +15 -3
- package/src/object-uris.js +9 -0
- package/src/reporter.js +33 -0
- package/src/tools/quality.js +9 -2
- package/src/tools/source.js +110 -19
- package/src/tools/transports.js +10 -2
package/README.md
CHANGED
|
@@ -198,9 +198,12 @@ so on startup. There are three channels:
|
|
|
198
198
|
missing capability) via the **`adt_report_issue`** tool.
|
|
199
199
|
|
|
200
200
|
What is sent: the sap-adt-mcp version, Node version, OS, the tool name, and the
|
|
201
|
-
error/finding with a fingerprint for de-duplication
|
|
202
|
-
|
|
203
|
-
|
|
201
|
+
error/finding with a fingerprint for de-duplication, plus an **anonymous install
|
|
202
|
+
id** (random bytes, cached at `~/.sap-adt-mcp/install-id`) that lets repeat
|
|
203
|
+
reports from the same install be grouped for triage — it identifies neither you
|
|
204
|
+
nor your system. Before anything leaves your machine it is scrubbed of
|
|
205
|
+
**hostnames, users, passwords, tokens, IPs, and emails**; tool arguments and
|
|
206
|
+
free-text fields are redacted the same way. Reports
|
|
204
207
|
go to a relay the maintainer owns, which files/de-dups a GitHub issue — the
|
|
205
208
|
relay holds the GitHub credentials, never this package.
|
|
206
209
|
|
|
@@ -310,8 +313,8 @@ or rejecting credentials. Run this first when troubleshooting.
|
|
|
310
313
|
|
|
311
314
|
| Tool | Purpose | Notes |
|
|
312
315
|
| --- | --- | --- |
|
|
313
|
-
| `adt_get_source` | Fetch ABAP source by object name + type. | Returns plain text. For classes, pick the include via `include`: `main` (default), `definitions`, `implementations`, `macros`, `testclasses`. Function modules require `group`. |
|
|
314
|
-
| `adt_set_source` | Replace source. Orchestrates lock → PUT → unlock. | Optional `transport`
|
|
316
|
+
| `adt_get_source` | Fetch ABAP source by object name + type. | Returns plain text. For classes, pick the include via `include`: `main` (default), `definitions`, `implementations`, `macros`, `testclasses`. Function modules require `group`. For large objects, pass `outputFile` to write the source straight to disk (response omits inline `source`). |
|
|
317
|
+
| `adt_set_source` | Replace source. Orchestrates lock → PUT → unlock. | Supply the new source inline via `source`, or via `sourceFile` (a local path the MCP reads itself) for large objects that exceed the per-call I/O cap. Optional `transport` assigns the change to a TR (`corrNr`); optional `lockHandle` reuses an externally-acquired lock. Refused under `readOnly: true`. |
|
|
315
318
|
| `adt_create_object` | Create a new ABAP object in a package. | Supported types: program, class, interface, include, functiongroup, function, cds, accesscontrol, metadataext, behaviordef, messageclass. After creation, set the source body with `adt_set_source` and activate. Refused under `readOnly: true`. |
|
|
316
319
|
| `adt_delete_object` | Delete an object. | Acquires lock and DELETEs. Refused under `readOnly: true`. |
|
|
317
320
|
| `adt_activate` | Activate one or more objects. | Pass `objects: [{ name, type, group? }]`. |
|
|
@@ -608,6 +611,25 @@ sticky-lock pattern:
|
|
|
608
611
|
The `lockHandle` parameter on `adt_set_source` skips internal lock/unlock
|
|
609
612
|
when present.
|
|
610
613
|
|
|
614
|
+
### Large objects (thousands of lines)
|
|
615
|
+
|
|
616
|
+
A multi-thousand-line class or program can exceed the per-call I/O cap, so its
|
|
617
|
+
source cannot be passed inline. Keep the content on disk instead — it never
|
|
618
|
+
enters the agent context:
|
|
619
|
+
|
|
620
|
+
```text
|
|
621
|
+
1. adt_get_source { object: "ZCL_BIG", type: "class",
|
|
622
|
+
outputFile: "/tmp/zcl_big.abap" } → writes the file, no inline source
|
|
623
|
+
2. (edit /tmp/zcl_big.abap locally)
|
|
624
|
+
3. adt_set_source { object: "ZCL_BIG", type: "class",
|
|
625
|
+
sourceFile: "/tmp/zcl_big.abap",
|
|
626
|
+
transport: "E4DK900123" } → MCP reads the file and PUTs it
|
|
627
|
+
```
|
|
628
|
+
|
|
629
|
+
Note: assigning to a transport works headless, but TR *creation*
|
|
630
|
+
(`adt_create_transport`) routes through a GUI dialog on some systems and can
|
|
631
|
+
fail with a 500 — pass an existing TR id instead.
|
|
632
|
+
|
|
611
633
|
## Caveats
|
|
612
634
|
|
|
613
635
|
- **NetWeaver release variation.** A few ADT endpoints (especially around
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sap-adt-mcp",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.50",
|
|
4
4
|
"mcpName": "io.github.yzonur/sap-adt-mcp",
|
|
5
5
|
"description": "MCP server giving Claude live access to SAP systems via ADT (ABAP Development Tools) REST. Read source, search, run syntax checks, and edit ABAP objects from any MCP-compatible client.",
|
|
6
6
|
"type": "module",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
},
|
|
22
22
|
"scripts": {
|
|
23
23
|
"start": "node src/server.js",
|
|
24
|
-
"test": "node --test test/adt-error.test.js test/atc-bulk.test.js test/audit.test.js test/cds.test.js test/config.test.js test/data-preview.test.js test/diff.test.js test/dump-feed.test.js test/grep-source.test.js test/jobs.test.js test/lifecycle-activate.test.js test/lock.test.js test/mcp-bug-fixes.test.js test/node-structure.test.js test/notes.test.js test/object-create.test.js test/object-references.test.js test/object-uris.test.js test/panel.test.js test/prompts.test.js test/provenance.test.js test/rap-scaffold.test.js test/reporter.test.js test/security.test.js test/source-chunked.test.js test/source-guard.test.js test/source-pagination.test.js test/syntax-context.test.js test/tools-shape.test.js test/versions.test.js test/worklist.test.js",
|
|
24
|
+
"test": "node --test test/adt-error.test.js test/atc-bulk.test.js test/audit.test.js test/cds.test.js test/config.test.js test/data-preview.test.js test/diff.test.js test/dump-feed.test.js test/grep-source.test.js test/jobs.test.js test/lifecycle-activate.test.js test/lock.test.js test/mcp-bug-fixes.test.js test/node-structure.test.js test/notes.test.js test/object-create.test.js test/object-references.test.js test/object-uris.test.js test/panel.test.js test/prompts.test.js test/provenance.test.js test/query-blank.test.js test/rap-scaffold.test.js test/reporter.test.js test/security.test.js test/source-chunked.test.js test/source-file-io.test.js test/source-guard.test.js test/source-pagination.test.js test/syntax-context.test.js test/tools-shape.test.js test/versions.test.js test/worklist.test.js",
|
|
25
25
|
"lint": "eslint src test"
|
|
26
26
|
},
|
|
27
27
|
"keywords": [
|
package/src/adt-client.js
CHANGED
|
@@ -108,7 +108,9 @@ export class AdtClient {
|
|
|
108
108
|
path: resolvedPath,
|
|
109
109
|
status: res.status,
|
|
110
110
|
ok: res.ok,
|
|
111
|
-
...(query?.corrNr
|
|
111
|
+
...(query?.corrNr && String(query.corrNr).trim()
|
|
112
|
+
? { transport: String(query.corrNr).trim() }
|
|
113
|
+
: {}),
|
|
112
114
|
});
|
|
113
115
|
}
|
|
114
116
|
|
|
@@ -285,7 +287,15 @@ export class AdtClient {
|
|
|
285
287
|
}
|
|
286
288
|
if (query) {
|
|
287
289
|
for (const [k, v] of Object.entries(query)) {
|
|
288
|
-
|
|
290
|
+
// A null/undefined/blank value is never a meaningful ADT query param, and
|
|
291
|
+
// a whitespace-only one is actively harmful: a transport passed as " "
|
|
292
|
+
// reaches the CTS backend as corrNr=%20 and 500s the request (#68). Skip
|
|
293
|
+
// empties uniformly so every corrNr-bearing tool (set_source, chunked,
|
|
294
|
+
// activate/delete, rap) is covered at one point.
|
|
295
|
+
if (v == null) continue;
|
|
296
|
+
const s = String(v);
|
|
297
|
+
if (s.trim() === "") continue;
|
|
298
|
+
url.searchParams.set(k, s);
|
|
289
299
|
}
|
|
290
300
|
}
|
|
291
301
|
return url.toString();
|
package/src/object-create.js
CHANGED
|
@@ -201,20 +201,32 @@ export function buildCreateRequest({
|
|
|
201
201
|
}
|
|
202
202
|
}
|
|
203
203
|
|
|
204
|
+
// The ADT object-create framework accepts a wildcard content type on the POST;
|
|
205
|
+
// abap-adt-api (the reference client) creates *every* object type with exactly
|
|
206
|
+
// this, never a resource-specific media type. It is our universal last resort.
|
|
207
|
+
export const GENERIC_CREATE_MEDIA_TYPE = "application/*";
|
|
208
|
+
|
|
204
209
|
// Most ADT create endpoints version their media type (…v2+xml, …v3+xml). The
|
|
205
210
|
// shapes above target the newest version a modern stack accepts, but older
|
|
206
211
|
// systems only know an earlier one and answer the newer media type with
|
|
207
212
|
// 415 ExceptionUnsupportedMediaType. Given a content-type, return the chain to
|
|
208
|
-
// try: the requested version first, then every lower version down to v1
|
|
209
|
-
//
|
|
213
|
+
// try: the requested version first, then every lower version down to v1, then
|
|
214
|
+
// the generic wildcard as a final catch-all — some systems register neither
|
|
215
|
+
// versioned type for a given object (e.g. DDLS/CDS create 415'd on both v2 and
|
|
216
|
+
// v1, #64), but the create framework always honours application/*.
|
|
210
217
|
export function mediaTypeFallbacks(contentType) {
|
|
211
218
|
const m = /^(.*\.)v(\d+)(\+xml)$/.exec(contentType);
|
|
212
|
-
if (!m)
|
|
219
|
+
if (!m) {
|
|
220
|
+
return contentType === GENERIC_CREATE_MEDIA_TYPE
|
|
221
|
+
? [contentType]
|
|
222
|
+
: [contentType, GENERIC_CREATE_MEDIA_TYPE];
|
|
223
|
+
}
|
|
213
224
|
const [, prefix, ver, suffix] = m;
|
|
214
225
|
const chain = [];
|
|
215
226
|
for (let v = Number(ver); v >= 1; v--) {
|
|
216
227
|
chain.push(`${prefix}v${v}${suffix}`);
|
|
217
228
|
}
|
|
229
|
+
chain.push(GENERIC_CREATE_MEDIA_TYPE);
|
|
218
230
|
return chain;
|
|
219
231
|
}
|
|
220
232
|
|
package/src/object-uris.js
CHANGED
|
@@ -86,6 +86,15 @@ function collapseSubtype(t) {
|
|
|
86
86
|
return t;
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
// The canonical base type: normalize aliases, then collapse a decorative
|
|
90
|
+
// TADIR subtype (DOMA/DD → DOMA) the same way objectUri/sourceUri do internally.
|
|
91
|
+
// Callers that dispatch on the type (e.g. the metadata-XML Accept lookup) must
|
|
92
|
+
// use this, not the raw normalizeType output — a subtype like "DOMA/DD" otherwise
|
|
93
|
+
// misses the table and falls through to a text/plain fetch that 406s (#66).
|
|
94
|
+
export function baseType(input) {
|
|
95
|
+
return collapseSubtype(normalizeType(input));
|
|
96
|
+
}
|
|
97
|
+
|
|
89
98
|
export function objectUri({ type, name, group }) {
|
|
90
99
|
const t = collapseSubtype(normalizeType(type));
|
|
91
100
|
if (typeof name !== "string" || name.length === 0) {
|
package/src/reporter.js
CHANGED
|
@@ -14,7 +14,9 @@
|
|
|
14
14
|
// Opt out with `"reporting": { "enabled": false }` in config, or SAP_ADT_MCP_REPORT=0.
|
|
15
15
|
|
|
16
16
|
import crypto from "node:crypto";
|
|
17
|
+
import fs from "node:fs";
|
|
17
18
|
import os from "node:os";
|
|
19
|
+
import path from "node:path";
|
|
18
20
|
import { BUILD_FINGERPRINT } from "./tools/_shared.js";
|
|
19
21
|
|
|
20
22
|
// Filled in by the maintainer after deploying the relay Worker. Overridable per
|
|
@@ -147,6 +149,33 @@ function hash16(s) {
|
|
|
147
149
|
return crypto.createHash("sha256").update(s).digest("hex").slice(0, 16);
|
|
148
150
|
}
|
|
149
151
|
|
|
152
|
+
// --- Install identity --------------------------------------------------------
|
|
153
|
+
|
|
154
|
+
// A stable, ANONYMOUS per-install identifier. It is random bytes — never derived
|
|
155
|
+
// from any machine, user, or config attribute — so it groups repeat reports from
|
|
156
|
+
// the same install for triage without identifying the person or their system.
|
|
157
|
+
// Generated once and cached at ~/.sap-adt-mcp/install-id, reused thereafter.
|
|
158
|
+
// Only touched when reporting is enabled. Best-effort: any fs failure falls back
|
|
159
|
+
// to an ephemeral id so a report is never blocked.
|
|
160
|
+
function resolveInstallId() {
|
|
161
|
+
const dir = path.join(os.homedir(), ".sap-adt-mcp");
|
|
162
|
+
const file = path.join(dir, "install-id");
|
|
163
|
+
try {
|
|
164
|
+
const existing = fs.readFileSync(file, "utf8").trim();
|
|
165
|
+
if (/^[0-9a-f]{16}$/i.test(existing)) return existing;
|
|
166
|
+
} catch {
|
|
167
|
+
// Not created yet (or unreadable) — fall through and mint one.
|
|
168
|
+
}
|
|
169
|
+
const id = crypto.randomBytes(8).toString("hex");
|
|
170
|
+
try {
|
|
171
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
172
|
+
fs.writeFileSync(file, id + "\n", { mode: 0o600 });
|
|
173
|
+
} catch {
|
|
174
|
+
// Couldn't persist — return an ephemeral id for this run rather than fail.
|
|
175
|
+
}
|
|
176
|
+
return id;
|
|
177
|
+
}
|
|
178
|
+
|
|
150
179
|
function normalizeText(s) {
|
|
151
180
|
return String(s ?? "")
|
|
152
181
|
.replace(/0x[0-9a-f]+/gi, "")
|
|
@@ -242,6 +271,9 @@ export function createReporter(config, pkg) {
|
|
|
242
271
|
const allowManual = rep.allowManual !== false;
|
|
243
272
|
const redact = makeRedactor(collectSecrets(config.systems));
|
|
244
273
|
const seen = new Set(); // per-process de-dup: one POST per fingerprint per run.
|
|
274
|
+
// Only mint/read the install-id when reporting is on, so an opted-out install
|
|
275
|
+
// writes nothing and sends nothing.
|
|
276
|
+
const installId = enabled ? resolveInstallId() : null;
|
|
245
277
|
|
|
246
278
|
// source = the x-report-source header (crash/adt-error use "sap-adt-mcp";
|
|
247
279
|
// agent-initiated reports use "sap-adt-mcp-manual"). The relay routes on it.
|
|
@@ -265,6 +297,7 @@ export function createReporter(config, pkg) {
|
|
|
265
297
|
return {
|
|
266
298
|
kind,
|
|
267
299
|
fingerprint,
|
|
300
|
+
install: installId,
|
|
268
301
|
build: BUILD_FINGERPRINT,
|
|
269
302
|
version: pkg.version,
|
|
270
303
|
node: process.version,
|
package/src/tools/quality.js
CHANGED
|
@@ -156,10 +156,17 @@ async function runAtcWorklist(client, sys, { uris, checkVariant, maxResults }) {
|
|
|
156
156
|
}
|
|
157
157
|
|
|
158
158
|
// Normalize a caller-supplied include context into an ADT object URI. Accepts a
|
|
159
|
-
// full ADT path as-is; treats
|
|
159
|
+
// full ADT path as-is; treats anything else as a program name.
|
|
160
|
+
//
|
|
161
|
+
// A real ADT object URI begins with the ADT path prefix. A *namespaced* ABAP
|
|
162
|
+
// name (e.g. "/FGLR/R_PO_ASSET_CREATE") also begins with "/", so keying on a
|
|
163
|
+
// leading slash alone misclassified it as a ready-made URI and produced an
|
|
164
|
+
// unmappable ?context= → 500 uriMappingError (#67). Only the ADT-path prefix
|
|
165
|
+
// marks a ready URI; every other value — bare or namespaced — is a program name
|
|
166
|
+
// whose slashes must be percent-encoded into the programs URI.
|
|
160
167
|
export function toContextUri(input) {
|
|
161
168
|
const s = String(input).trim();
|
|
162
|
-
if (s.startsWith("/")) return s;
|
|
169
|
+
if (s.startsWith("/sap/bc/adt/")) return s;
|
|
163
170
|
return `/sap/bc/adt/programs/programs/${encodeURIComponent(s.toLowerCase())}`;
|
|
164
171
|
}
|
|
165
172
|
|
package/src/tools/source.js
CHANGED
|
@@ -1,8 +1,36 @@
|
|
|
1
|
-
import
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { sourceUri, objectUri, normalizeType, baseType, METADATA_XML_ACCEPT } from "../object-uris.js";
|
|
2
4
|
import { acquireLock, releaseLock } from "../lock.js";
|
|
3
5
|
import { errorResult, jsonResult, textResult } from "../result.js";
|
|
4
6
|
import { OBJECT_TYPE_HINT, SYSTEM_HINT } from "./_shared.js";
|
|
5
7
|
|
|
8
|
+
// Read a local file for a write. The MCP process reads it directly, so a large
|
|
9
|
+
// object's source never has to travel back through the agent's per-call I/O cap
|
|
10
|
+
// (#39). fs errors become a clean { ok:false } instead of a crash.
|
|
11
|
+
function readLocalFile(p) {
|
|
12
|
+
try {
|
|
13
|
+
return { ok: true, content: fs.readFileSync(p, "utf8") };
|
|
14
|
+
} catch (err) {
|
|
15
|
+
const why = err.code === "ENOENT" ? "file not found" : err.message;
|
|
16
|
+
return { ok: false, error: why };
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Write fetched source straight to disk (symmetric to readLocalFile) so a big
|
|
21
|
+
// read → edit → write round-trip can stay entirely on the filesystem. Creates
|
|
22
|
+
// the parent directory if needed.
|
|
23
|
+
function writeLocalFile(p, content) {
|
|
24
|
+
try {
|
|
25
|
+
const dir = path.dirname(p);
|
|
26
|
+
if (dir) fs.mkdirSync(dir, { recursive: true });
|
|
27
|
+
fs.writeFileSync(p, content);
|
|
28
|
+
return { ok: true };
|
|
29
|
+
} catch (err) {
|
|
30
|
+
return { ok: false, error: err.message };
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
6
34
|
const TOP_LEVEL_KEYWORDS = [
|
|
7
35
|
"CLASS",
|
|
8
36
|
"INTERFACE",
|
|
@@ -120,7 +148,7 @@ export const tools = [
|
|
|
120
148
|
{
|
|
121
149
|
name: "adt_get_source",
|
|
122
150
|
description:
|
|
123
|
-
"Fetch the ABAP source of an object (program, class, interface, function module, include, CDS, table). Returns plain text. DDIC primitives without plain-text source (data element, domain, message class) return their ADT XML metadata instead (format: 'xml'). For sources larger than the MCP per-call output cap (~64 KB), use firstLine/lastLine to paginate or onlyMethod to slice a single method body.",
|
|
151
|
+
"Fetch the ABAP source of an object (program, class, interface, function module, include, CDS, table). Returns plain text. DDIC primitives without plain-text source (data element, domain, message class) return their ADT XML metadata instead (format: 'xml'). For sources larger than the MCP per-call output cap (~64 KB), use firstLine/lastLine to paginate or onlyMethod to slice a single method body — or pass `outputFile` to write the full source straight to disk (then edit it and write it back with adt_set_source `sourceFile`, keeping large objects entirely off the agent's I/O path).",
|
|
124
152
|
inputSchema: {
|
|
125
153
|
type: "object",
|
|
126
154
|
properties: {
|
|
@@ -152,6 +180,11 @@ export const tools = [
|
|
|
152
180
|
description:
|
|
153
181
|
"Return only the METHOD <name> ... ENDMETHOD block (case-insensitive). Returns metadata about the slice (startLine, endLine). Convenient for inspecting one method of a large class without paginating manually.",
|
|
154
182
|
},
|
|
183
|
+
outputFile: {
|
|
184
|
+
type: "string",
|
|
185
|
+
description:
|
|
186
|
+
"Local file path to write the source to instead of returning it inline. The response carries metadata (bytesWritten, totalLines) but omits `source`, so an arbitrarily large object can be fetched without hitting the output cap. Respects firstLine/lastLine/onlyMethod when set (otherwise writes the full source).",
|
|
187
|
+
},
|
|
155
188
|
},
|
|
156
189
|
required: ["object", "type"],
|
|
157
190
|
},
|
|
@@ -159,7 +192,7 @@ export const tools = [
|
|
|
159
192
|
{
|
|
160
193
|
name: "adt_set_source",
|
|
161
194
|
description:
|
|
162
|
-
"Replace the ABAP source of an object. The
|
|
195
|
+
"Replace the ABAP source of an object. The new source is the FULL text of the include — it ATOMICALLY REPLACES the entire include on the server. Passing a partial chunk or diff will delete the rest of the include. Supply the source inline via `source`, OR — for a large object that would blow the per-call I/O cap — via `sourceFile` (a local path the MCP process reads itself). Orchestrates lock → PUT → unlock automatically. Requires read-only mode to be off for the target system.",
|
|
163
196
|
inputSchema: {
|
|
164
197
|
type: "object",
|
|
165
198
|
properties: {
|
|
@@ -175,7 +208,12 @@ export const tools = [
|
|
|
175
208
|
source: {
|
|
176
209
|
type: "string",
|
|
177
210
|
description:
|
|
178
|
-
"New ABAP source code — FULL text of the include. Atomic replace. Do not pass a partial diff.",
|
|
211
|
+
"New ABAP source code — FULL text of the include. Atomic replace. Do not pass a partial diff. Mutually exclusive with `sourceFile`.",
|
|
212
|
+
},
|
|
213
|
+
sourceFile: {
|
|
214
|
+
type: "string",
|
|
215
|
+
description:
|
|
216
|
+
"Local file path whose contents become the new FULL source. The MCP process reads the file, so there is no size ceiling — use this for large objects (e.g. a multi-thousand-line class) that cannot be passed inline. Mutually exclusive with `source`.",
|
|
179
217
|
},
|
|
180
218
|
transport: {
|
|
181
219
|
type: "string",
|
|
@@ -193,7 +231,7 @@ export const tools = [
|
|
|
193
231
|
"Set to true to bypass the partial-source guard (e.g. when writing a content-free or fragment include intentionally). The guard rejects sources whose first non-comment line does not start with a recognized top-level ABAP construct keyword.",
|
|
194
232
|
},
|
|
195
233
|
},
|
|
196
|
-
required: ["object", "type"
|
|
234
|
+
required: ["object", "type"],
|
|
197
235
|
},
|
|
198
236
|
},
|
|
199
237
|
{
|
|
@@ -273,23 +311,35 @@ export function register({ getClient }) {
|
|
|
273
311
|
return {
|
|
274
312
|
adt_get_source: async (args) => {
|
|
275
313
|
if (typeof args.object !== "string" || args.object.length === 0) {
|
|
276
|
-
const
|
|
277
|
-
args.
|
|
278
|
-
? "
|
|
279
|
-
:
|
|
314
|
+
const wrong =
|
|
315
|
+
args.object_name !== undefined
|
|
316
|
+
? "object_name"
|
|
317
|
+
: args.name !== undefined
|
|
318
|
+
? "name"
|
|
319
|
+
: null;
|
|
320
|
+
const hint = wrong ? ` (you passed \`${wrong}\` — the field is \`object\`)` : "";
|
|
280
321
|
return textResult(
|
|
281
322
|
`adt_get_source: \`object\` is required${hint}. Also use \`firstLine\`/\`lastLine\` (not \`line\`/\`endLine\`) to paginate.`,
|
|
282
323
|
true
|
|
283
324
|
);
|
|
284
325
|
}
|
|
285
326
|
if (typeof args.type !== "string" || args.type.length === 0) {
|
|
286
|
-
|
|
327
|
+
const hint =
|
|
328
|
+
args.object_type !== undefined
|
|
329
|
+
? " (you passed `object_type` — the field is `type`)"
|
|
330
|
+
: "";
|
|
331
|
+
return textResult(
|
|
332
|
+
`adt_get_source: \`type\` is required${hint} (e.g. 'class', 'program', 'dataelement').`,
|
|
333
|
+
true
|
|
334
|
+
);
|
|
287
335
|
}
|
|
288
336
|
const { client, name: sys } = getClient(args.system);
|
|
289
337
|
let t;
|
|
290
338
|
let path;
|
|
291
339
|
try {
|
|
292
|
-
|
|
340
|
+
// Collapse decorative subtypes (DOMA/DD → DOMA) so the metadata-XML
|
|
341
|
+
// Accept lookup below matches; sourceUri collapses internally already.
|
|
342
|
+
t = baseType(args.type);
|
|
293
343
|
path = sourceUri({
|
|
294
344
|
type: args.type,
|
|
295
345
|
name: args.object,
|
|
@@ -315,17 +365,24 @@ export function register({ getClient }) {
|
|
|
315
365
|
const res = await client.request({ path, accept: metaAccept });
|
|
316
366
|
const text = await res.text();
|
|
317
367
|
if (!res.ok) return errorResult(sys, res.status, text, res.headers.get("content-type"));
|
|
318
|
-
|
|
368
|
+
const xmlMeta = {
|
|
319
369
|
system: sys,
|
|
320
370
|
object: args.object,
|
|
321
371
|
type: t,
|
|
322
372
|
path,
|
|
323
373
|
format: "xml",
|
|
324
|
-
source: text,
|
|
325
374
|
bytes: text.length,
|
|
326
375
|
totalLines: text.split(/\r?\n/).length,
|
|
327
376
|
note: `${t} has no plain-text source; returning ADT XML metadata (${metaAccept.split("+")[0]}+xml).`,
|
|
328
|
-
}
|
|
377
|
+
};
|
|
378
|
+
if (args.outputFile) {
|
|
379
|
+
const w = writeLocalFile(args.outputFile, text);
|
|
380
|
+
if (!w.ok) {
|
|
381
|
+
return errorResult(sys, 500, `Could not write outputFile ${JSON.stringify(args.outputFile)}: ${w.error}`, "text/plain", { stage: "output" });
|
|
382
|
+
}
|
|
383
|
+
return jsonResult({ ...xmlMeta, outputFile: args.outputFile, bytesWritten: text.length });
|
|
384
|
+
}
|
|
385
|
+
return jsonResult({ ...xmlMeta, source: text });
|
|
329
386
|
}
|
|
330
387
|
|
|
331
388
|
const res = await client.request({ path, accept: "text/plain" });
|
|
@@ -374,12 +431,11 @@ export function register({ getClient }) {
|
|
|
374
431
|
}
|
|
375
432
|
|
|
376
433
|
const slice = allLines.slice(sliceFirst - 1, sliceLast).join("\n");
|
|
377
|
-
|
|
434
|
+
const meta = {
|
|
378
435
|
system: sys,
|
|
379
436
|
object: args.object,
|
|
380
437
|
type: normalizeType(args.type),
|
|
381
438
|
path,
|
|
382
|
-
source: slice,
|
|
383
439
|
bytes: slice.length,
|
|
384
440
|
lines: sliceLast - sliceFirst + 1,
|
|
385
441
|
totalLines,
|
|
@@ -388,13 +444,48 @@ export function register({ getClient }) {
|
|
|
388
444
|
lastLine: sliceLast,
|
|
389
445
|
scope,
|
|
390
446
|
truncated: scope !== "full",
|
|
391
|
-
}
|
|
447
|
+
};
|
|
448
|
+
if (args.outputFile) {
|
|
449
|
+
const w = writeLocalFile(args.outputFile, slice);
|
|
450
|
+
if (!w.ok) {
|
|
451
|
+
return errorResult(sys, 500, `Could not write outputFile ${JSON.stringify(args.outputFile)}: ${w.error}`, "text/plain", { stage: "output" });
|
|
452
|
+
}
|
|
453
|
+
return jsonResult({ ...meta, outputFile: args.outputFile, bytesWritten: slice.length });
|
|
454
|
+
}
|
|
455
|
+
return jsonResult({ ...meta, source: slice });
|
|
392
456
|
},
|
|
393
457
|
|
|
394
458
|
adt_set_source: async (args) => {
|
|
395
459
|
const { client, name: sys } = getClient(args.system);
|
|
460
|
+
|
|
461
|
+
// Resolve the new source from either inline `source` or a local
|
|
462
|
+
// `sourceFile` (read here by the MCP, so large objects bypass the agent's
|
|
463
|
+
// I/O cap — #39). Exactly one of the two must be supplied.
|
|
464
|
+
const hasInline = typeof args.source === "string";
|
|
465
|
+
const hasFile = typeof args.sourceFile === "string" && args.sourceFile.length > 0;
|
|
466
|
+
if (hasInline && hasFile) {
|
|
467
|
+
return textResult("adt_set_source: pass either `source` or `sourceFile`, not both.", true);
|
|
468
|
+
}
|
|
469
|
+
if (!hasInline && !hasFile) {
|
|
470
|
+
return textResult(
|
|
471
|
+
"adt_set_source: `source` (inline full text) or `sourceFile` (local path) is required.",
|
|
472
|
+
true
|
|
473
|
+
);
|
|
474
|
+
}
|
|
475
|
+
let source = args.source;
|
|
476
|
+
if (hasFile) {
|
|
477
|
+
const rd = readLocalFile(args.sourceFile);
|
|
478
|
+
if (!rd.ok) {
|
|
479
|
+
return textResult(
|
|
480
|
+
`adt_set_source: could not read sourceFile ${JSON.stringify(args.sourceFile)}: ${rd.error}.`,
|
|
481
|
+
true
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
source = rd.content;
|
|
485
|
+
}
|
|
486
|
+
|
|
396
487
|
if (!args.acknowledgePartial) {
|
|
397
|
-
const partialReason = detectPartialSource(
|
|
488
|
+
const partialReason = detectPartialSource(source);
|
|
398
489
|
if (partialReason) {
|
|
399
490
|
return errorResult(sys, 422, partialReason, "text/plain", {
|
|
400
491
|
stage: "validate",
|
|
@@ -438,7 +529,7 @@ export function register({ getClient }) {
|
|
|
438
529
|
"Content-Type": "text/plain; charset=utf-8",
|
|
439
530
|
"X-sap-adt-sessiontype": "stateful",
|
|
440
531
|
},
|
|
441
|
-
body:
|
|
532
|
+
body: source,
|
|
442
533
|
});
|
|
443
534
|
const putText = await putRes.text();
|
|
444
535
|
if (!putRes.ok) {
|
package/src/tools/transports.js
CHANGED
|
@@ -42,7 +42,7 @@ export const tools = [
|
|
|
42
42
|
{
|
|
43
43
|
name: "adt_create_transport",
|
|
44
44
|
description:
|
|
45
|
-
"Create a new transport request. Returns the new TR number. Subject to read-only mode.",
|
|
45
|
+
"Create a new transport request. Returns the new TR number. Subject to read-only mode. Note: on some systems TR *creation* routes through a SAP GUI dialog and fails headless with a 500 (SAPLSTRD/SAPLSPO4 'No window system type'). Assigning changes to an EXISTING request works headless — pass that TR id as `transport` to adt_lock / adt_set_source instead.",
|
|
46
46
|
inputSchema: {
|
|
47
47
|
type: "object",
|
|
48
48
|
properties: {
|
|
@@ -118,10 +118,18 @@ export function register({ getClient }) {
|
|
|
118
118
|
adt_create_transport: async (args) => {
|
|
119
119
|
const { client, name: sys, profile } = getClient(args.system);
|
|
120
120
|
const trType = args.type ?? "K";
|
|
121
|
+
// Only emit tm:target when a real target is given. Sending tm:target=""
|
|
122
|
+
// (the old default) makes some systems reject the create with an opaque
|
|
123
|
+
// 500 — the same blank-value hazard as a whitespace corrNr (#68/#63).
|
|
124
|
+
// Omitting it lets CTS apply the connection's default consolidation route.
|
|
125
|
+
const targetAttr =
|
|
126
|
+
typeof args.target === "string" && args.target.trim()
|
|
127
|
+
? ` tm:target="${escapeXml(args.target.trim())}"`
|
|
128
|
+
: "";
|
|
121
129
|
const xml =
|
|
122
130
|
`<?xml version="1.0" encoding="UTF-8"?>` +
|
|
123
131
|
`<tm:root xmlns:tm="http://www.sap.com/cts/adt/tm" tm:useraction="newrequest">` +
|
|
124
|
-
`<tm:request tm:desc="${escapeXml(args.description)}" tm:type="${trType}"
|
|
132
|
+
`<tm:request tm:desc="${escapeXml(args.description)}" tm:type="${trType}"${targetAttr} tm:cliDep="X">` +
|
|
125
133
|
`<tm:user tm:name="${escapeXml(profile.user.toUpperCase())}"/>` +
|
|
126
134
|
`</tm:request>` +
|
|
127
135
|
`</tm:root>`;
|