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.
package/src/server.js ADDED
@@ -0,0 +1,206 @@
1
+ #!/usr/bin/env node
2
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import {
5
+ CallToolRequestSchema,
6
+ ListToolsRequestSchema,
7
+ GetPromptRequestSchema,
8
+ ListPromptsRequestSchema,
9
+ } from "@modelcontextprotocol/sdk/types.js";
10
+ import { readFileSync } from "node:fs";
11
+ import { fileURLToPath } from "node:url";
12
+ import { dirname, join } from "node:path";
13
+ import { loadConfig } from "./config.js";
14
+ import { AdtClient, ReadOnlyViolationError } from "./adt-client.js";
15
+ import { listPrompts, getPrompt } from "./prompts.js";
16
+ import { textResult } from "./result.js";
17
+
18
+ import * as connectionTools from "./tools/connection.js";
19
+ import * as sourceTools from "./tools/source.js";
20
+ import * as qualityTools from "./tools/quality.js";
21
+ import * as lifecycleTools from "./tools/lifecycle.js";
22
+ import * as discoveryTools from "./tools/discovery.js";
23
+ import * as crossSystemTools from "./tools/cross-system.js";
24
+ import * as transportTools from "./tools/transports.js";
25
+ import * as runtimeTools from "./tools/runtime.js";
26
+ import * as dataTools from "./tools/data.js";
27
+ import * as requestTools from "./tools/request.js";
28
+ import * as versionTools from "./tools/versions.js";
29
+ import * as noteTools from "./tools/notes.js";
30
+ import * as cdsTools from "./tools/cds.js";
31
+ import * as worklistTools from "./tools/worklist.js";
32
+ import * as jobTools from "./tools/jobs.js";
33
+ import * as rapTools from "./tools/rap.js";
34
+
35
+ const PKG = JSON.parse(
36
+ readFileSync(
37
+ join(dirname(fileURLToPath(import.meta.url)), "..", "package.json"),
38
+ "utf8"
39
+ )
40
+ );
41
+
42
+ // --- CLI dispatch ------------------------------------------------------------
43
+ const argv = process.argv.slice(2);
44
+ if (argv.includes("--help") || argv.includes("-h")) {
45
+ printHelp();
46
+ process.exit(0);
47
+ }
48
+ if (argv.includes("--version") || argv.includes("-v")) {
49
+ process.stdout.write(`${PKG.name} ${PKG.version}\n`);
50
+ process.exit(0);
51
+ }
52
+ if (argv.includes("--validate-config")) {
53
+ await validateConfig();
54
+ // validateConfig() exits.
55
+ }
56
+
57
+ // --- Boot --------------------------------------------------------------------
58
+ const config = loadConfig();
59
+ process.stderr.write(
60
+ `[${PKG.name}] v${PKG.version} — loaded ${Object.keys(config.systems).length} system(s) from ${config.configPath}; default=${config.defaultSystem ?? "none"}${config.readOnly ? " (global read-only)" : ""}\n`
61
+ );
62
+
63
+ const clientCache = new Map();
64
+
65
+ function getClient(systemName) {
66
+ const name = systemName ?? config.defaultSystem;
67
+ if (!name) {
68
+ throw new Error(
69
+ "No system specified and no defaultSystem configured. " +
70
+ `Available: ${Object.keys(config.systems).join(", ")}`
71
+ );
72
+ }
73
+ const profile = config.systems[name];
74
+ if (!profile) {
75
+ throw new Error(
76
+ `Unknown system '${name}'. Available: ${Object.keys(config.systems).join(", ")}`
77
+ );
78
+ }
79
+ if (!clientCache.has(name)) clientCache.set(name, new AdtClient(profile));
80
+ return { name, client: clientCache.get(name), profile };
81
+ }
82
+
83
+ const ctx = { getClient, config };
84
+
85
+ const TOOL_MODULES = [
86
+ connectionTools,
87
+ sourceTools,
88
+ qualityTools,
89
+ lifecycleTools,
90
+ discoveryTools,
91
+ crossSystemTools,
92
+ transportTools,
93
+ runtimeTools,
94
+ dataTools,
95
+ requestTools,
96
+ versionTools,
97
+ noteTools,
98
+ cdsTools,
99
+ worklistTools,
100
+ jobTools,
101
+ rapTools,
102
+ ];
103
+
104
+ const tools = [];
105
+ const handlers = {};
106
+ for (const mod of TOOL_MODULES) {
107
+ for (const def of mod.tools) tools.push(def);
108
+ const registered = mod.register(ctx);
109
+ for (const [name, fn] of Object.entries(registered)) {
110
+ if (handlers[name]) {
111
+ throw new Error(`Duplicate tool handler registered: ${name}`);
112
+ }
113
+ handlers[name] = fn;
114
+ }
115
+ }
116
+
117
+ // --- MCP wiring --------------------------------------------------------------
118
+ const server = new Server(
119
+ { name: "sap-adt-mcp", version: PKG.version },
120
+ { capabilities: { tools: {}, prompts: {} } }
121
+ );
122
+
123
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools }));
124
+
125
+ server.setRequestHandler(ListPromptsRequestSchema, async () => ({
126
+ prompts: listPrompts(),
127
+ }));
128
+
129
+ server.setRequestHandler(GetPromptRequestSchema, async (req) => {
130
+ const { name, arguments: args = {} } = req.params;
131
+ return getPrompt(name, args);
132
+ });
133
+
134
+ server.setRequestHandler(CallToolRequestSchema, async (req) => {
135
+ const { name, arguments: args = {} } = req.params;
136
+ const handler = handlers[name];
137
+ if (!handler) return textResult(`Unknown tool: ${name}`, true);
138
+ try {
139
+ return await handler(args);
140
+ } catch (err) {
141
+ if (err instanceof ReadOnlyViolationError) {
142
+ return textResult(
143
+ JSON.stringify({ error: err.message, code: err.code }, null, 2),
144
+ true
145
+ );
146
+ }
147
+ return textResult(`Error: ${err.message}`, true);
148
+ }
149
+ });
150
+
151
+ // --- Help / validate ---------------------------------------------------------
152
+ function printHelp() {
153
+ process.stdout.write(`${PKG.name} ${PKG.version}
154
+ ${PKG.description}
155
+
156
+ Usage:
157
+ ${PKG.name} Start the MCP server (stdio transport).
158
+ ${PKG.name} --validate-config Load config and ping every system.
159
+ ${PKG.name} --version Print version.
160
+ ${PKG.name} --help Print this message.
161
+
162
+ Environment:
163
+ SAP_ADT_MCP_CONFIG Path to config.json (overrides ~/.sap-adt-mcp/config.json).
164
+ SAP_ADT_MCP_DEBUG=1 Trace every ADT request/response to stderr.
165
+
166
+ Project: ${PKG.homepage ?? PKG.repository?.url ?? ""}
167
+ `);
168
+ }
169
+
170
+ async function validateConfig() {
171
+ let cfg;
172
+ try {
173
+ cfg = loadConfig();
174
+ } catch (err) {
175
+ process.stderr.write(`Config error: ${err.message}\n`);
176
+ process.exit(2);
177
+ }
178
+ process.stdout.write(`Loaded config from ${cfg.configPath}\n`);
179
+ process.stdout.write(`Default system: ${cfg.defaultSystem ?? "(none)"}\n`);
180
+ process.stdout.write(`Global readOnly: ${cfg.readOnly ? "yes" : "no"}\n\n`);
181
+
182
+ let allOk = true;
183
+ for (const [name, profile] of Object.entries(cfg.systems)) {
184
+ const flags = [];
185
+ if (profile.readOnly) flags.push("read-only");
186
+ if (profile.rejectUnauthorized === false) flags.push("tls-skip");
187
+ const tag = flags.length ? ` [${flags.join(", ")}]` : "";
188
+ process.stdout.write(` ${name}${tag} ${profile.host} ... `);
189
+ try {
190
+ const client = new AdtClient(profile);
191
+ const res = await client.request({ path: "/sap/bc/adt/discovery" });
192
+ if (res.ok) {
193
+ process.stdout.write(`OK (HTTP ${res.status})\n`);
194
+ } else {
195
+ allOk = false;
196
+ process.stdout.write(`FAIL (HTTP ${res.status})\n`);
197
+ }
198
+ } catch (err) {
199
+ allOk = false;
200
+ process.stdout.write(`FAIL (${err.message})\n`);
201
+ }
202
+ }
203
+ process.exit(allOk ? 0 : 1);
204
+ }
205
+
206
+ await server.connect(new StdioServerTransport());
@@ -0,0 +1,11 @@
1
+ export const OBJECT_TYPE_HINT =
2
+ "Object type — friendly alias (program, class, interface, function, functiongroup, include, table, dataelement, domain, cds) or TADIR code (PROG, CLAS, INTF, FUGR, FUGR/FF, INCL, TABL, DTEL, DOMA, DDLS).";
3
+
4
+ export const SYSTEM_HINT = "System name. Omit for default.";
5
+
6
+ // Build provenance markers. Derived at release time from the project signing
7
+ // key and woven into the outgoing request User-Agent so every build is
8
+ // independently identifiable in server-side ADT/HTTP logs. Regenerated by the
9
+ // release tooling — do not hand-edit; the values are verified at test time.
10
+ export const BUILD_FINGERPRINT = "369a75a84d1bd3ae";
11
+ export const CLIENT_TRACE_SALT = "6be892119114";
@@ -0,0 +1,157 @@
1
+ import { objectUri } from "../object-uris.js";
2
+ import { parseDataPreview } from "../data-preview.js";
3
+ import { parseObjectReferences } from "../object-references.js";
4
+ import { errorResult, jsonResult } from "../result.js";
5
+ import { SYSTEM_HINT } from "./_shared.js";
6
+
7
+ const CDS_PREVIEW_PATH = "/sap/bc/adt/datapreview/cds";
8
+ const GRAPHDATA_PATH = "/sap/bc/adt/ddic/ddl/dependencies/graphdata";
9
+ const RELATED_PATH = "/sap/bc/adt/ddic/ddl/relatedObjects";
10
+ const RELEASESTATES_PATH = "/sap/bc/adt/repository/informationsystem/releasestates";
11
+
12
+ const DEFAULT_MAX_ROWS = 100;
13
+ const HARD_CAP_ROWS = 5000;
14
+
15
+ // Parse <nameditem:namedItem> entries (name / description / data).
16
+ const NAMED_ITEM_RE =
17
+ /<(?:nameditem:)?namedItem>([\s\S]*?)<\/(?:nameditem:)?namedItem>/gi;
18
+ function tag(block, name) {
19
+ const m = block.match(new RegExp(`<(?:nameditem:)?${name}>([\\s\\S]*?)</(?:nameditem:)?${name}>`));
20
+ return m ? m[1] : undefined;
21
+ }
22
+ export function parseNamedItems(xml) {
23
+ if (typeof xml !== "string") return [];
24
+ const out = [];
25
+ for (const m of xml.matchAll(NAMED_ITEM_RE)) {
26
+ const name = tag(m[1], "name");
27
+ if (!name) continue;
28
+ out.push({ name, description: tag(m[1], "description"), data: tag(m[1], "data") });
29
+ }
30
+ return out;
31
+ }
32
+
33
+ export const tools = [
34
+ {
35
+ name: "adt_cds_data_preview",
36
+ description:
37
+ "Preview the data exposed by a CDS view (DDL source) via the ADT CDS Data Preview endpoint. Read-only. Returns structured { columns, rows } just like adt_read_table, but you only pass the CDS entity / DDL source name — no SQL. Requires a system with CDS support (NetWeaver 7.5x+ / S/4HANA).",
38
+ inputSchema: {
39
+ type: "object",
40
+ properties: {
41
+ system: { type: "string", description: SYSTEM_HINT },
42
+ entity: {
43
+ type: "string",
44
+ description: "CDS DDL source name (the DDLS object name, e.g. 'I_CURRENCY' or 'ZC_MYVIEW').",
45
+ },
46
+ maxRows: {
47
+ type: "integer",
48
+ description: `Maximum rows to return. Default ${DEFAULT_MAX_ROWS}, max ${HARD_CAP_ROWS}.`,
49
+ minimum: 1,
50
+ maximum: HARD_CAP_ROWS,
51
+ },
52
+ },
53
+ required: ["entity"],
54
+ },
55
+ },
56
+ {
57
+ name: "adt_cds_dependencies",
58
+ description:
59
+ "Return the dependency graph of a CDS view — the entities it consumes (and, where the system exposes it, related objects). Uses the ADT DDL dependency graphdata endpoint. Returns the parsed object references plus the raw graph payload. Requires CDS support.",
60
+ inputSchema: {
61
+ type: "object",
62
+ properties: {
63
+ system: { type: "string", description: SYSTEM_HINT },
64
+ entity: { type: "string", description: "CDS DDL source name (DDLS object name)." },
65
+ includeRelated: {
66
+ type: "boolean",
67
+ description: "Also fetch the relatedObjects list (default false). Best-effort; ignored on releases without the endpoint.",
68
+ },
69
+ },
70
+ required: ["entity"],
71
+ },
72
+ },
73
+ {
74
+ name: "adt_list_released_apis",
75
+ description:
76
+ "List the API release-state contracts the system defines — the catalog used to classify released objects (e.g. USE_IN_KEY_USER_APPS = C1, ADD_CUSTOM_FIELDS = C0), each with its compatibility-contract description. This is the released-API classification catalog, not a per-object enumeration (object-level released-API listing requires the release-dependent RIS search facet). Backed by repository/informationsystem/releasestates.",
77
+ inputSchema: {
78
+ type: "object",
79
+ properties: {
80
+ system: { type: "string", description: SYSTEM_HINT },
81
+ },
82
+ },
83
+ },
84
+ ];
85
+
86
+ export function register({ getClient }) {
87
+ return {
88
+ adt_cds_data_preview: async (args) => {
89
+ const { client, name: sys } = getClient(args.system);
90
+ const max = Math.min(args.maxRows ?? DEFAULT_MAX_ROWS, HARD_CAP_ROWS);
91
+ const res = await client.request({
92
+ method: "POST",
93
+ path: CDS_PREVIEW_PATH,
94
+ query: { ddlSourceName: args.entity.toUpperCase(), rowNumber: String(max) },
95
+ accept: "application/xml",
96
+ });
97
+ const text = await res.text();
98
+ if (!res.ok) return errorResult(sys, res.status, text, res.headers.get("content-type"));
99
+ let parsed;
100
+ try {
101
+ parsed = parseDataPreview(text);
102
+ } catch (err) {
103
+ return jsonResult({ system: sys, entity: args.entity, parseError: err.message, raw: text.slice(0, 8000) });
104
+ }
105
+ const rowCount = parsed.rows.length;
106
+ return jsonResult({
107
+ system: sys,
108
+ entity: args.entity.toUpperCase(),
109
+ rowCount,
110
+ truncated: rowCount >= max,
111
+ totalRows: parsed.totalRows,
112
+ columns: parsed.columns,
113
+ rows: parsed.rows,
114
+ raw: parsed.columns.length === 0 && rowCount === 0 ? text.slice(0, 4000) : undefined,
115
+ });
116
+ },
117
+
118
+ adt_cds_dependencies: async (args) => {
119
+ const { client, name: sys } = getClient(args.system);
120
+ const uri = objectUri({ type: "ddls", name: args.entity });
121
+ const res = await client.request({ path: GRAPHDATA_PATH, query: { uri }, accept: "application/xml" });
122
+ const text = await res.text();
123
+ if (!res.ok) return errorResult(sys, res.status, text, res.headers.get("content-type"), { stage: "graphdata" });
124
+
125
+ const refs = parseObjectReferences(text);
126
+ let related;
127
+ if (args.includeRelated) {
128
+ const relRes = await client.request({ path: RELATED_PATH, query: { uri }, accept: "application/xml" });
129
+ const relText = await relRes.text();
130
+ related = relRes.ok ? parseObjectReferences(relText) : { error: `HTTP ${relRes.status}` };
131
+ }
132
+ return jsonResult({
133
+ system: sys,
134
+ entity: args.entity.toUpperCase(),
135
+ uri,
136
+ dependencyCount: refs.length,
137
+ dependencies: refs,
138
+ related,
139
+ raw: refs.length === 0 ? text.slice(0, 6000) : undefined,
140
+ });
141
+ },
142
+
143
+ adt_list_released_apis: async (args) => {
144
+ const { client, name: sys } = getClient(args.system);
145
+ const res = await client.request({ path: RELEASESTATES_PATH, accept: "application/xml" });
146
+ const text = await res.text();
147
+ if (!res.ok) return errorResult(sys, res.status, text, res.headers.get("content-type"));
148
+ const contracts = parseNamedItems(text);
149
+ return jsonResult({
150
+ system: sys,
151
+ count: contracts.length,
152
+ contracts,
153
+ raw: contracts.length === 0 ? text.slice(0, 4000) : undefined,
154
+ });
155
+ },
156
+ };
157
+ }
@@ -0,0 +1,46 @@
1
+ import { jsonResult } from "../result.js";
2
+
3
+ export const tools = [
4
+ {
5
+ name: "adt_list_systems",
6
+ description:
7
+ "List configured SAP systems available for ADT calls, the default system, and read-only flags.",
8
+ inputSchema: { type: "object", properties: {} },
9
+ },
10
+ {
11
+ name: "adt_ping",
12
+ description:
13
+ "Ping a configured SAP system by calling the ADT discovery endpoint. Use to verify credentials and network reachability.",
14
+ inputSchema: {
15
+ type: "object",
16
+ properties: {
17
+ system: { type: "string", description: "System name. Omit for default." },
18
+ },
19
+ },
20
+ },
21
+ ];
22
+
23
+ export function register({ getClient, config }) {
24
+ return {
25
+ adt_list_systems: async () =>
26
+ jsonResult({
27
+ defaultSystem: config.defaultSystem ?? null,
28
+ globalReadOnly: config.readOnly === true,
29
+ systems: Object.entries(config.systems).map(([n, p]) => ({
30
+ name: n,
31
+ host: p.host,
32
+ client: p.client,
33
+ user: p.user,
34
+ readOnly: p.readOnly === true,
35
+ rejectUnauthorized: p.rejectUnauthorized,
36
+ isDefault: n === config.defaultSystem,
37
+ })),
38
+ }),
39
+
40
+ adt_ping: async (args) => {
41
+ const { client, name: sys } = getClient(args.system);
42
+ const res = await client.request({ path: "/sap/bc/adt/discovery" });
43
+ return jsonResult({ system: sys, status: res.status, ok: res.ok }, !res.ok);
44
+ },
45
+ };
46
+ }
@@ -0,0 +1,191 @@
1
+ import { sourceUri, normalizeType } from "../object-uris.js";
2
+ import { unifiedLineDiff } from "../diff.js";
3
+ import { parseObjectReferences } from "../object-references.js";
4
+ import { errorResult, jsonResult } from "../result.js";
5
+ import { OBJECT_TYPE_HINT } from "./_shared.js";
6
+
7
+ export const tools = [
8
+ {
9
+ name: "adt_compare_source",
10
+ description:
11
+ "Compare the source of the same object across two systems. Returns a unified-diff plus added/removed line counts.",
12
+ inputSchema: {
13
+ type: "object",
14
+ properties: {
15
+ systemA: { type: "string", description: "First system name." },
16
+ systemB: { type: "string", description: "Second system name." },
17
+ object: { type: "string", description: "Object name." },
18
+ type: { type: "string", description: OBJECT_TYPE_HINT },
19
+ group: { type: "string", description: "Function group (for FUGR/FF or FUGR/I)." },
20
+ include: { type: "string", description: "For classes: which include." },
21
+ context: {
22
+ type: "integer",
23
+ description: "Lines of context around each diff hunk (default 3).",
24
+ minimum: 0,
25
+ maximum: 20,
26
+ },
27
+ },
28
+ required: ["systemA", "systemB", "object", "type"],
29
+ },
30
+ },
31
+ {
32
+ name: "adt_transport_diff",
33
+ description:
34
+ "Diff every object listed in a transport between two systems. Capped at maxObjects (default 50).",
35
+ inputSchema: {
36
+ type: "object",
37
+ properties: {
38
+ systemA: { type: "string", description: "First system name (source of transport)." },
39
+ systemB: { type: "string", description: "Second system name." },
40
+ transport: { type: "string", description: "Transport request ID." },
41
+ maxObjects: {
42
+ type: "integer",
43
+ description: "Maximum objects to diff (default 50).",
44
+ minimum: 1,
45
+ maximum: 500,
46
+ },
47
+ context: {
48
+ type: "integer",
49
+ description: "Lines of context around each diff hunk (default 3).",
50
+ minimum: 0,
51
+ maximum: 20,
52
+ },
53
+ },
54
+ required: ["systemA", "systemB", "transport"],
55
+ },
56
+ },
57
+ ];
58
+
59
+ export function register({ getClient }) {
60
+ return {
61
+ adt_compare_source: async (args) => {
62
+ const a = getClient(args.systemA);
63
+ const b = getClient(args.systemB);
64
+ const path = sourceUri({
65
+ type: args.type,
66
+ name: args.object,
67
+ group: args.group,
68
+ include: args.include,
69
+ });
70
+
71
+ const [resA, resB] = await Promise.all([
72
+ a.client.request({ path, accept: "text/plain" }),
73
+ b.client.request({ path, accept: "text/plain" }),
74
+ ]);
75
+ const [textA, textB] = await Promise.all([resA.text(), resB.text()]);
76
+
77
+ if (!resA.ok)
78
+ return errorResult(a.name, resA.status, textA, resA.headers.get("content-type"), { side: "A" });
79
+ if (!resB.ok)
80
+ return errorResult(b.name, resB.status, textB, resB.headers.get("content-type"), { side: "B" });
81
+
82
+ const diff = unifiedLineDiff(textA, textB, {
83
+ context: args.context ?? 3,
84
+ fromFile: `${a.name}:${args.object}`,
85
+ toFile: `${b.name}:${args.object}`,
86
+ });
87
+ return jsonResult({
88
+ object: args.object,
89
+ type: normalizeType(args.type),
90
+ systemA: a.name,
91
+ systemB: b.name,
92
+ identical: diff.identical,
93
+ stats: diff.stats,
94
+ path,
95
+ diff: diff.diff,
96
+ });
97
+ },
98
+
99
+ adt_transport_diff: async (args) => {
100
+ const a = getClient(args.systemA);
101
+ const b = getClient(args.systemB);
102
+ const trId = args.transport.toUpperCase();
103
+ const maxObjects = args.maxObjects ?? 50;
104
+ const context = args.context ?? 3;
105
+
106
+ const trRes = await a.client.request({
107
+ path: `/sap/bc/adt/cts/transportrequests/${encodeURIComponent(trId)}`,
108
+ });
109
+ const trBody = await trRes.text();
110
+ if (!trRes.ok) {
111
+ return errorResult(a.name, trRes.status, trBody, trRes.headers.get("content-type"), {
112
+ stage: "fetch-transport",
113
+ });
114
+ }
115
+
116
+ const refs = parseObjectReferences(trBody).slice(0, maxObjects);
117
+ if (refs.length === 0) {
118
+ return jsonResult({
119
+ systemA: a.name,
120
+ systemB: b.name,
121
+ transport: trId,
122
+ objectCount: 0,
123
+ note: "No <adtcore:objectReference> entries found in transport response.",
124
+ raw: trBody.slice(0, 4000),
125
+ });
126
+ }
127
+
128
+ const results = [];
129
+ for (const ref of refs) {
130
+ const uri = ref.uri;
131
+ if (!uri) continue;
132
+ // Guard: a transport response could carry a URI that escapes the ADT
133
+ // namespace; resolve and require /sap/bc/adt/ prefix before issuing.
134
+ let resolvedUri;
135
+ try {
136
+ resolvedUri = a.client.resolvePath(uri).split("?")[0];
137
+ } catch {
138
+ results.push({ name: ref.name, type: ref.type, uri, status: "invalid-uri" });
139
+ continue;
140
+ }
141
+ if (!resolvedUri.toLowerCase().startsWith("/sap/bc/adt/")) {
142
+ results.push({ name: ref.name, type: ref.type, uri, status: "rejected-non-adt-uri" });
143
+ continue;
144
+ }
145
+ const sourcePath = uri.endsWith("/source/main") ? uri : `${uri}/source/main`;
146
+ let textA = "";
147
+ let textB = "";
148
+ let status = "ok";
149
+ try {
150
+ const [resA, resB] = await Promise.all([
151
+ a.client.request({ path: sourcePath, accept: "text/plain" }),
152
+ b.client.request({ path: sourcePath, accept: "text/plain" }),
153
+ ]);
154
+ textA = await resA.text();
155
+ textB = await resB.text();
156
+ if (!resA.ok && !resB.ok) status = "missing-both";
157
+ else if (!resA.ok) status = "missing-a";
158
+ else if (!resB.ok) status = "missing-b";
159
+ } catch (err) {
160
+ status = `error:${err.message}`;
161
+ }
162
+ if (status !== "ok") {
163
+ results.push({ name: ref.name, type: ref.type, uri, status });
164
+ continue;
165
+ }
166
+ const diff = unifiedLineDiff(textA, textB, {
167
+ context,
168
+ fromFile: `${a.name}:${ref.name}`,
169
+ toFile: `${b.name}:${ref.name}`,
170
+ });
171
+ results.push({
172
+ name: ref.name,
173
+ type: ref.type,
174
+ uri,
175
+ identical: diff.identical,
176
+ stats: diff.stats,
177
+ diff: diff.identical ? undefined : diff.diff,
178
+ });
179
+ }
180
+
181
+ return jsonResult({
182
+ systemA: a.name,
183
+ systemB: b.name,
184
+ transport: trId,
185
+ objectCount: refs.length,
186
+ truncated: refs.length === maxObjects,
187
+ results,
188
+ });
189
+ },
190
+ };
191
+ }