@topolo/mcp 0.10.2 → 0.10.3
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/dist/dispatch.d.ts +5 -0
- package/dist/dispatch.js +64 -2
- package/dist/http.d.ts +26 -3
- package/dist/http.js +326 -214
- package/dist/index.js +768 -91
- package/package.json +4 -8
package/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
5
|
|
|
6
|
-
//
|
|
6
|
+
// ../../../development-workflow-acceleration/node_modules/.pnpm/@topolo+sdk@0.10.4/node_modules/@topolo/sdk/dist/index.js
|
|
7
7
|
function applyAuthHeaders(headers, credential) {
|
|
8
8
|
if (credential.kind === "api_key") {
|
|
9
9
|
headers.set("X-Api-Key", credential.apiKey);
|
|
@@ -30,6 +30,90 @@ var TopoloSdkError = class extends Error {
|
|
|
30
30
|
this.code = code;
|
|
31
31
|
}
|
|
32
32
|
};
|
|
33
|
+
var PUBLIC_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
34
|
+
"action_app_identity_unavailable",
|
|
35
|
+
"action_authentication_required",
|
|
36
|
+
"action_catalog_summary_unavailable",
|
|
37
|
+
"action_catalog_unavailable",
|
|
38
|
+
"action_contract_mismatch",
|
|
39
|
+
"action_permission_required",
|
|
40
|
+
"action_runtime_authorization_required",
|
|
41
|
+
"action_service_not_allowed",
|
|
42
|
+
"auth_error",
|
|
43
|
+
"authentication_required",
|
|
44
|
+
"authorization_pending",
|
|
45
|
+
"bad_sig",
|
|
46
|
+
"confirmation_required",
|
|
47
|
+
"dependency_unavailable",
|
|
48
|
+
"environment_mismatch",
|
|
49
|
+
"environment_service_url_required",
|
|
50
|
+
"expired_token",
|
|
51
|
+
"forbidden",
|
|
52
|
+
"http_error",
|
|
53
|
+
"immutable_drift",
|
|
54
|
+
"integration_dependency_unavailable",
|
|
55
|
+
"integration_misconfigured",
|
|
56
|
+
"invalid_action",
|
|
57
|
+
"invalid_action_input",
|
|
58
|
+
"invalid_grant",
|
|
59
|
+
"invalid_input",
|
|
60
|
+
"invalid_token",
|
|
61
|
+
"mcp_error",
|
|
62
|
+
"membership_required",
|
|
63
|
+
"network_error",
|
|
64
|
+
"organization_context_required",
|
|
65
|
+
"organization_enrollment_required",
|
|
66
|
+
"permission_denied",
|
|
67
|
+
"rate_limited",
|
|
68
|
+
"request_failed",
|
|
69
|
+
"service_catalog_unavailable",
|
|
70
|
+
"service_path_origin_mismatch",
|
|
71
|
+
"service_required",
|
|
72
|
+
"slow_down",
|
|
73
|
+
"state_conflict",
|
|
74
|
+
"unauthorized",
|
|
75
|
+
"unknown_action",
|
|
76
|
+
"unknown_service"
|
|
77
|
+
]);
|
|
78
|
+
var ERROR_CODE_PATTERN = /^[a-z][a-z0-9_]{0,63}$/;
|
|
79
|
+
var TOPOLO_REQUEST_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
80
|
+
var CLOUDFLARE_RAY_ID_PATTERN = /^[0-9a-f]{16}-[A-Z]{3}$/;
|
|
81
|
+
var PERMISSION_PATTERN = /^[a-z][a-z0-9_-]{0,63}(?:\.[a-z][a-z0-9_-]{0,63})*:(?:[a-z][a-z0-9_-]{0,63}|\*)$/;
|
|
82
|
+
var RETRY_AFTER_DATE_PATTERN = /^(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun), \d{2} (?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) \d{4} \d{2}:\d{2}:\d{2} GMT$/;
|
|
83
|
+
var PUBLIC_DETAIL_SOURCES = /* @__PURE__ */ new Set([
|
|
84
|
+
"api_key_introspection",
|
|
85
|
+
"api_key_validation",
|
|
86
|
+
"auth_context",
|
|
87
|
+
"auth_validate",
|
|
88
|
+
"catalog",
|
|
89
|
+
"credential",
|
|
90
|
+
"runtime",
|
|
91
|
+
"service_token_provider",
|
|
92
|
+
"service_verify_key",
|
|
93
|
+
"token_validation",
|
|
94
|
+
"upstream"
|
|
95
|
+
]);
|
|
96
|
+
function canonicalTopoloErrorCode(value) {
|
|
97
|
+
if (typeof value !== "string") return null;
|
|
98
|
+
const candidate = value.trim().toLowerCase();
|
|
99
|
+
if (!ERROR_CODE_PATTERN.test(candidate) || !PUBLIC_ERROR_CODES.has(candidate)) return null;
|
|
100
|
+
return candidate;
|
|
101
|
+
}
|
|
102
|
+
function canonicalTopoloRequestId(value) {
|
|
103
|
+
if (typeof value !== "string") return void 0;
|
|
104
|
+
const candidate = value.trim();
|
|
105
|
+
return TOPOLO_REQUEST_ID_PATTERN.test(candidate) || CLOUDFLARE_RAY_ID_PATTERN.test(candidate) ? candidate : void 0;
|
|
106
|
+
}
|
|
107
|
+
function canonicalTopoloRetryAfter(value) {
|
|
108
|
+
if (typeof value !== "string") return void 0;
|
|
109
|
+
const candidate = value.trim();
|
|
110
|
+
if (/^\d{1,10}$/.test(candidate) || RETRY_AFTER_DATE_PATTERN.test(candidate)) return candidate;
|
|
111
|
+
return void 0;
|
|
112
|
+
}
|
|
113
|
+
function canonicalTopoloRequired(value) {
|
|
114
|
+
if (!Array.isArray(value)) return [];
|
|
115
|
+
return [...new Set(value.filter((item) => typeof item === "string" && PERMISSION_PATTERN.test(item)))].slice(0, 32);
|
|
116
|
+
}
|
|
33
117
|
var TopoloAuthError = class extends TopoloSdkError {
|
|
34
118
|
status;
|
|
35
119
|
body;
|
|
@@ -37,15 +121,19 @@ var TopoloAuthError = class extends TopoloSdkError {
|
|
|
37
121
|
path;
|
|
38
122
|
requestId;
|
|
39
123
|
retryAfter;
|
|
124
|
+
details;
|
|
40
125
|
constructor(message, code = "auth_error", options = {}) {
|
|
41
|
-
super(code, message);
|
|
126
|
+
super(canonicalTopoloErrorCode(code) ?? "auth_error", message);
|
|
42
127
|
this.name = "TopoloAuthError";
|
|
43
128
|
this.status = options.status ?? 401;
|
|
44
129
|
this.body = options.body;
|
|
45
130
|
this.service = options.service;
|
|
46
131
|
this.path = options.path;
|
|
47
|
-
this.requestId = options.requestId;
|
|
48
|
-
this.retryAfter = options.retryAfter;
|
|
132
|
+
this.requestId = canonicalTopoloRequestId(options.requestId);
|
|
133
|
+
this.retryAfter = canonicalTopoloRetryAfter(options.retryAfter);
|
|
134
|
+
this.details = extractTopoloServiceErrorDetails(
|
|
135
|
+
options.details === void 0 ? options.body : { details: options.details }
|
|
136
|
+
);
|
|
49
137
|
}
|
|
50
138
|
};
|
|
51
139
|
var TopoloPermissionError = class extends TopoloSdkError {
|
|
@@ -56,28 +144,140 @@ var TopoloPermissionError = class extends TopoloSdkError {
|
|
|
56
144
|
path;
|
|
57
145
|
requestId;
|
|
58
146
|
retryAfter;
|
|
147
|
+
details;
|
|
59
148
|
constructor(message, required, options = {}) {
|
|
60
|
-
super(
|
|
149
|
+
super(
|
|
150
|
+
canonicalTopoloErrorCode(options.code) ?? serviceErrorCode(options.body, "permission_denied"),
|
|
151
|
+
message
|
|
152
|
+
);
|
|
61
153
|
this.name = "TopoloPermissionError";
|
|
62
|
-
this.required = required;
|
|
154
|
+
this.required = canonicalTopoloRequired(required);
|
|
63
155
|
this.status = options.status ?? 403;
|
|
64
156
|
this.body = options.body;
|
|
65
157
|
this.service = options.service;
|
|
66
158
|
this.path = options.path;
|
|
67
|
-
this.requestId = options.requestId;
|
|
68
|
-
this.retryAfter = options.retryAfter;
|
|
159
|
+
this.requestId = canonicalTopoloRequestId(options.requestId);
|
|
160
|
+
this.retryAfter = canonicalTopoloRetryAfter(options.retryAfter);
|
|
161
|
+
this.details = extractTopoloServiceErrorDetails(
|
|
162
|
+
options.details === void 0 ? options.body : { details: options.details }
|
|
163
|
+
);
|
|
69
164
|
}
|
|
70
165
|
};
|
|
71
|
-
function
|
|
72
|
-
if (!body || typeof body !== "object" || Array.isArray(body)) return
|
|
166
|
+
function extractTopoloServiceErrorCode(body) {
|
|
167
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) return null;
|
|
73
168
|
const record = body;
|
|
74
|
-
|
|
169
|
+
const topLevel = canonicalTopoloErrorCode(record.code);
|
|
170
|
+
if (topLevel) return topLevel;
|
|
75
171
|
const nested = record.error;
|
|
172
|
+
const nestedString = canonicalTopoloErrorCode(nested);
|
|
173
|
+
if (nestedString) return nestedString;
|
|
76
174
|
if (nested && typeof nested === "object" && !Array.isArray(nested)) {
|
|
77
175
|
const code = nested.code;
|
|
78
|
-
|
|
176
|
+
const nestedCode = canonicalTopoloErrorCode(code);
|
|
177
|
+
if (nestedCode) return nestedCode;
|
|
79
178
|
}
|
|
80
|
-
return
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
function serviceErrorCode(body, fallback) {
|
|
182
|
+
return extractTopoloServiceErrorCode(body) ?? fallback;
|
|
183
|
+
}
|
|
184
|
+
function extractTopoloServiceErrorDetails(body) {
|
|
185
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) return void 0;
|
|
186
|
+
const record = body;
|
|
187
|
+
let candidate = record.details;
|
|
188
|
+
const nested = record.error;
|
|
189
|
+
if (nested && typeof nested === "object" && !Array.isArray(nested)) {
|
|
190
|
+
const nestedRecord = nested;
|
|
191
|
+
if (candidate === void 0) candidate = nestedRecord.details ?? nestedRecord;
|
|
192
|
+
}
|
|
193
|
+
if (candidate === void 0) candidate = record;
|
|
194
|
+
if (!candidate || typeof candidate !== "object" || Array.isArray(candidate)) return void 0;
|
|
195
|
+
const source = candidate;
|
|
196
|
+
const safe = {};
|
|
197
|
+
for (const [key, value] of Object.entries(source)) {
|
|
198
|
+
if (/present$/i.test(key) && typeof value === "boolean") {
|
|
199
|
+
safe[key] = value;
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
if (key === "retryable" && typeof value === "boolean") {
|
|
203
|
+
safe.retryable = value;
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
if (key === "code") {
|
|
207
|
+
const sanitized = canonicalTopoloErrorCode(value);
|
|
208
|
+
if (sanitized) safe.code = sanitized;
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
if (key === "source") {
|
|
212
|
+
const sanitized = typeof value === "string" ? value.trim().toLowerCase() : "";
|
|
213
|
+
if (PUBLIC_DETAIL_SOURCES.has(sanitized)) safe.source = sanitized;
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
if (key === "dependency" || key === "platform") {
|
|
217
|
+
safe[`${key}Present`] = value !== null && value !== void 0;
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
if (key === "error" && !safe.code) {
|
|
221
|
+
const sanitized = canonicalTopoloErrorCode(value);
|
|
222
|
+
if (sanitized) safe.code = sanitized;
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
if (key === "required" && Array.isArray(value)) {
|
|
226
|
+
const required = canonicalTopoloRequired(value);
|
|
227
|
+
if (required.length) safe.required = required;
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
if (["missingFields", "fields", "paths"].includes(key) && Array.isArray(value)) {
|
|
231
|
+
const paths = value.filter((item) => typeof item === "string" && /^[$.a-z0-9_[\]-]{1,160}$/i.test(item));
|
|
232
|
+
if (paths.length) safe[key] = paths;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return Object.keys(safe).length ? safe : void 0;
|
|
236
|
+
}
|
|
237
|
+
function publicTopoloErrorMessage(error) {
|
|
238
|
+
const code = canonicalTopoloErrorCode(
|
|
239
|
+
typeof error === "string" ? error : error.code ?? extractTopoloServiceErrorCode(error.body)
|
|
240
|
+
) ?? "request_failed";
|
|
241
|
+
if (/(?:organization|org).*(?:enroll|membership|member|admission)/.test(code)) {
|
|
242
|
+
return "Organization or application enrollment is required.";
|
|
243
|
+
}
|
|
244
|
+
if (/(?:organization|org).*(?:context|required|missing|invalid|unknown|not_found)/.test(code)) {
|
|
245
|
+
return "Organization context is unavailable.";
|
|
246
|
+
}
|
|
247
|
+
if (["auth_error", "authentication_required", "unauthorized", "invalid_token", "expired_token"].includes(code)) {
|
|
248
|
+
return "Authentication failed.";
|
|
249
|
+
}
|
|
250
|
+
if (code === "permission_denied" || code === "forbidden") return "Permission denied.";
|
|
251
|
+
if (code === "confirmation_required") return "Confirmation is required.";
|
|
252
|
+
return `Topolo request failed (${code}).`;
|
|
253
|
+
}
|
|
254
|
+
function topoloErrorHint(error) {
|
|
255
|
+
const code = canonicalTopoloErrorCode(
|
|
256
|
+
typeof error === "string" ? error : error.code ?? extractTopoloServiceErrorCode(error.body)
|
|
257
|
+
);
|
|
258
|
+
if (!code) return void 0;
|
|
259
|
+
if (code === "auth_error" || code === "authentication_required" || code === "unauthorized" || code === "invalid_token" || code === "expired_token") {
|
|
260
|
+
return "Run `topolo auth login`, then verify the effective Topolo identity and organization.";
|
|
261
|
+
}
|
|
262
|
+
if (code === "confirmation_required") {
|
|
263
|
+
return "Review the unchanged action plan, obtain approval, and retry once with --confirm or confirm:true.";
|
|
264
|
+
}
|
|
265
|
+
if (code === "enrollment_required" || code === "membership_required" || /(?:organization|org).*(?:enroll|membership|member|admission)|(?:enroll|membership).*(?:organization|org)/.test(code)) {
|
|
266
|
+
return "The current identity is not enrolled for this organization or application. Use an enrolled identity or ask an organization administrator to complete enrollment.";
|
|
267
|
+
}
|
|
268
|
+
if (/(?:organization|org).*(?:context|required|missing|invalid|unknown|not_found)/.test(code)) {
|
|
269
|
+
return "Select a signed-in organization context, then verify the effective identity and organization before retrying.";
|
|
270
|
+
}
|
|
271
|
+
if (code === "permission_denied" || code === "forbidden") {
|
|
272
|
+
return "The current identity or resource context is not authorized. Review the structured details and published authorization contract.";
|
|
273
|
+
}
|
|
274
|
+
if (code === "environment_mismatch") {
|
|
275
|
+
return "Use service origins and credentials from one explicit Topolo environment only.";
|
|
276
|
+
}
|
|
277
|
+
if (code === "invalid_action_input" || code === "invalid_input") {
|
|
278
|
+
return "Correct only the reported schema paths, then prepare the action again.";
|
|
279
|
+
}
|
|
280
|
+
return void 0;
|
|
81
281
|
}
|
|
82
282
|
var TopoloConfirmationRequiredError = class extends TopoloSdkError {
|
|
83
283
|
constructor(message) {
|
|
@@ -92,17 +292,66 @@ var TopoloHttpError = class extends TopoloSdkError {
|
|
|
92
292
|
path;
|
|
93
293
|
requestId;
|
|
94
294
|
retryAfter;
|
|
295
|
+
details;
|
|
95
296
|
constructor(service, path, status, body, message, options = {}) {
|
|
96
|
-
super(
|
|
297
|
+
super(serviceErrorCode(body, "http_error"), message ?? `HTTP ${status} from ${service}${path}`);
|
|
97
298
|
this.name = "TopoloHttpError";
|
|
98
299
|
this.status = status;
|
|
99
300
|
this.body = body;
|
|
100
301
|
this.service = service;
|
|
101
302
|
this.path = path;
|
|
102
|
-
this.requestId = options.requestId;
|
|
103
|
-
this.retryAfter = options.retryAfter;
|
|
303
|
+
this.requestId = canonicalTopoloRequestId(options.requestId);
|
|
304
|
+
this.retryAfter = canonicalTopoloRetryAfter(options.retryAfter);
|
|
305
|
+
this.details = extractTopoloServiceErrorDetails(
|
|
306
|
+
options.details === void 0 ? body : { details: options.details }
|
|
307
|
+
);
|
|
104
308
|
}
|
|
105
309
|
};
|
|
310
|
+
function hostnameHasSuffix(hostname, suffix) {
|
|
311
|
+
return hostname === suffix || hostname.endsWith(`.${suffix}`);
|
|
312
|
+
}
|
|
313
|
+
function workersDevAccountMatches(hostname, account) {
|
|
314
|
+
if (!hostname.endsWith(".workers.dev")) return false;
|
|
315
|
+
const labels = hostname.slice(0, -".workers.dev".length).split(".");
|
|
316
|
+
return labels.length >= 2 && labels.at(-1) === account;
|
|
317
|
+
}
|
|
318
|
+
function originMatchesEnvironment(url, environment) {
|
|
319
|
+
const hostname = url.hostname.toLowerCase();
|
|
320
|
+
if (environment === "local") {
|
|
321
|
+
return hostname === "localhost" || hostname.endsWith(".localhost") || hostname === "127.0.0.1" || hostname === "[::1]";
|
|
322
|
+
}
|
|
323
|
+
if (url.protocol !== "https:") return false;
|
|
324
|
+
if (environment === "production") {
|
|
325
|
+
return hostnameHasSuffix(hostname, "topolo.app") || hostnameHasSuffix(hostname, "topolo.io") || workersDevAccountMatches(hostname, "topolo");
|
|
326
|
+
}
|
|
327
|
+
if (environment === "staging") {
|
|
328
|
+
return hostnameHasSuffix(hostname, "stg.topolo.us") || workersDevAccountMatches(hostname, "topolo-staging");
|
|
329
|
+
}
|
|
330
|
+
return hostnameHasSuffix(hostname, "topolo.dev") || workersDevAccountMatches(hostname, "topolo-development");
|
|
331
|
+
}
|
|
332
|
+
function assertTopoloServiceUrlEnvironment(service, value, environment) {
|
|
333
|
+
let url;
|
|
334
|
+
try {
|
|
335
|
+
url = new URL(value);
|
|
336
|
+
} catch {
|
|
337
|
+
throw new TopoloSdkError("invalid_service_url", `Invalid URL configured for Topolo service "${service}".`);
|
|
338
|
+
}
|
|
339
|
+
if (url.protocol !== "https:" && url.protocol !== "http:" || url.username || url.password || !originMatchesEnvironment(url, environment)) {
|
|
340
|
+
throw new TopoloSdkError(
|
|
341
|
+
"environment_mismatch",
|
|
342
|
+
`Refusing Topolo service "${service}": ${url.origin} does not belong to ${environment}.`
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
return url.origin;
|
|
346
|
+
}
|
|
347
|
+
function topoloServiceOrigins(serviceUrls, environment) {
|
|
348
|
+
return Object.fromEntries(
|
|
349
|
+
Object.entries(serviceUrls ?? {}).sort(([left], [right]) => left.localeCompare(right)).map(([service, value]) => [
|
|
350
|
+
service,
|
|
351
|
+
assertTopoloServiceUrlEnvironment(service, value, environment)
|
|
352
|
+
])
|
|
353
|
+
);
|
|
354
|
+
}
|
|
106
355
|
var APP_API_URLS = {
|
|
107
356
|
auth: "https://auth.topolo.app",
|
|
108
357
|
developers: "https://developers.topolo.app"
|
|
@@ -2851,6 +3100,7 @@ var TopoloClient = class _TopoloClient {
|
|
|
2851
3100
|
credential;
|
|
2852
3101
|
agent;
|
|
2853
3102
|
serviceUrls;
|
|
3103
|
+
environment;
|
|
2854
3104
|
appIds;
|
|
2855
3105
|
requireConfirmForWrites;
|
|
2856
3106
|
timeoutMs;
|
|
@@ -2879,7 +3129,8 @@ var TopoloClient = class _TopoloClient {
|
|
|
2879
3129
|
if (!options.agent?.clientName) throw new TopoloAuthError("agent.clientName is required");
|
|
2880
3130
|
this.credential = options.credential;
|
|
2881
3131
|
this.agent = options.agent;
|
|
2882
|
-
this.
|
|
3132
|
+
this.environment = options.environment;
|
|
3133
|
+
this.serviceUrls = options.environment ? topoloServiceOrigins(options.serviceUrls, options.environment) : options.serviceUrls;
|
|
2883
3134
|
this.appIds = options.appIds;
|
|
2884
3135
|
this.requireConfirmForWrites = options.requireConfirmForWrites !== false;
|
|
2885
3136
|
this.timeoutMs = options.timeoutMs ?? 3e4;
|
|
@@ -2908,9 +3159,15 @@ var TopoloClient = class _TopoloClient {
|
|
|
2908
3159
|
);
|
|
2909
3160
|
}
|
|
2910
3161
|
await this.loadCurrentCredential();
|
|
2911
|
-
const resolvedService = opts.baseUrl ? { serviceKey: normalizeLookupKey(opts.service), baseUrl:
|
|
3162
|
+
const resolvedService = opts.baseUrl ? { serviceKey: normalizeLookupKey(opts.service), baseUrl: this.validatedServiceOrigin(opts.service, opts.baseUrl) } : await this.resolveRequestService(opts.service);
|
|
2912
3163
|
const baseUrl = resolvedService.baseUrl;
|
|
2913
3164
|
const url = new URL(opts.path, baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`);
|
|
3165
|
+
if (url.origin !== new URL(baseUrl).origin) {
|
|
3166
|
+
throw new TopoloSdkError(
|
|
3167
|
+
"service_path_origin_mismatch",
|
|
3168
|
+
`Refusing Topolo request path because it changes the resolved ${resolvedService.serviceKey || opts.service} service origin.`
|
|
3169
|
+
);
|
|
3170
|
+
}
|
|
2914
3171
|
if (opts.query) {
|
|
2915
3172
|
for (const [k, v] of Object.entries(opts.query)) {
|
|
2916
3173
|
if (v !== void 0) url.searchParams.set(k, String(v));
|
|
@@ -2981,7 +3238,7 @@ var TopoloClient = class _TopoloClient {
|
|
|
2981
3238
|
url: urlStr,
|
|
2982
3239
|
requestId,
|
|
2983
3240
|
durationMs: Date.now() - startedAt,
|
|
2984
|
-
error:
|
|
3241
|
+
error: "network_error"
|
|
2985
3242
|
});
|
|
2986
3243
|
throw err;
|
|
2987
3244
|
}
|
|
@@ -2990,6 +3247,7 @@ var TopoloClient = class _TopoloClient {
|
|
|
2990
3247
|
if (!res.ok) {
|
|
2991
3248
|
const durationMs = Date.now() - startedAt;
|
|
2992
3249
|
const message = describeError(parsed, `HTTP ${res.status}`);
|
|
3250
|
+
const publicCode = extractTopoloServiceErrorCode(parsed) ?? "http_error";
|
|
2993
3251
|
this.emit({
|
|
2994
3252
|
phase: "error",
|
|
2995
3253
|
method,
|
|
@@ -2998,7 +3256,7 @@ var TopoloClient = class _TopoloClient {
|
|
|
2998
3256
|
url: urlStr,
|
|
2999
3257
|
requestId,
|
|
3000
3258
|
durationMs,
|
|
3001
|
-
error:
|
|
3259
|
+
error: publicCode,
|
|
3002
3260
|
status: res.status
|
|
3003
3261
|
});
|
|
3004
3262
|
const errorContext = {
|
|
@@ -3006,17 +3264,21 @@ var TopoloClient = class _TopoloClient {
|
|
|
3006
3264
|
body: parsed,
|
|
3007
3265
|
service: resolvedService.serviceKey,
|
|
3008
3266
|
path: opts.path,
|
|
3009
|
-
requestId: res.headers.get("X-Topolo-Request-Id")
|
|
3010
|
-
retryAfter: res.headers.get("Retry-After")
|
|
3267
|
+
requestId: canonicalTopoloRequestId(res.headers.get("X-Topolo-Request-Id")) ?? requestId,
|
|
3268
|
+
retryAfter: canonicalTopoloRetryAfter(res.headers.get("Retry-After"))
|
|
3011
3269
|
};
|
|
3012
3270
|
if (res.status === 401) {
|
|
3013
|
-
throw new TopoloAuthError(
|
|
3271
|
+
throw new TopoloAuthError(
|
|
3272
|
+
describeError(parsed, "Unauthorized"),
|
|
3273
|
+
extractTopoloServiceErrorCode(parsed) ?? "auth_error",
|
|
3274
|
+
errorContext
|
|
3275
|
+
);
|
|
3014
3276
|
}
|
|
3015
3277
|
if (res.status === 403) {
|
|
3016
3278
|
throw new TopoloPermissionError(
|
|
3017
3279
|
describeError(parsed, "Permission denied"),
|
|
3018
3280
|
extractRequired(parsed),
|
|
3019
|
-
errorContext
|
|
3281
|
+
{ ...errorContext, code: extractTopoloServiceErrorCode(parsed) ?? "permission_denied" }
|
|
3020
3282
|
);
|
|
3021
3283
|
}
|
|
3022
3284
|
throw new TopoloHttpError(
|
|
@@ -3066,6 +3328,17 @@ var TopoloClient = class _TopoloClient {
|
|
|
3066
3328
|
});
|
|
3067
3329
|
return this.refreshInFlight;
|
|
3068
3330
|
}
|
|
3331
|
+
validatedServiceOrigin(service, value) {
|
|
3332
|
+
const origin = requireHttpOrigin(value);
|
|
3333
|
+
return this.environment ? assertTopoloServiceUrlEnvironment(service, origin, this.environment) : origin;
|
|
3334
|
+
}
|
|
3335
|
+
validatedActionEntry(entry) {
|
|
3336
|
+
if (!entry.apiBaseUrl) return entry;
|
|
3337
|
+
return {
|
|
3338
|
+
...entry,
|
|
3339
|
+
apiBaseUrl: this.validatedServiceOrigin(entry.appSlug || entry.appId, entry.apiBaseUrl)
|
|
3340
|
+
};
|
|
3341
|
+
}
|
|
3069
3342
|
async resolveRequestService(service) {
|
|
3070
3343
|
const serviceKey = normalizeLookupKey(service);
|
|
3071
3344
|
if (!serviceKey) {
|
|
@@ -3073,17 +3346,20 @@ var TopoloClient = class _TopoloClient {
|
|
|
3073
3346
|
}
|
|
3074
3347
|
if (serviceKey === "auth" || serviceKey === "developers") {
|
|
3075
3348
|
const overrideUrl = resolveConfiguredServiceUrl(serviceKey, this.serviceUrls);
|
|
3076
|
-
if (overrideUrl) return { serviceKey, baseUrl: overrideUrl };
|
|
3349
|
+
if (overrideUrl) return { serviceKey, baseUrl: this.validatedServiceOrigin(serviceKey, overrideUrl) };
|
|
3077
3350
|
if (serviceKey === "developers") {
|
|
3078
3351
|
const derived = deriveDevelopersFromAuth(resolveConfiguredServiceUrl("auth", this.serviceUrls));
|
|
3079
|
-
if (derived) return { serviceKey, baseUrl: derived };
|
|
3352
|
+
if (derived) return { serviceKey, baseUrl: this.validatedServiceOrigin(serviceKey, derived) };
|
|
3080
3353
|
}
|
|
3081
|
-
return {
|
|
3354
|
+
return {
|
|
3355
|
+
serviceKey,
|
|
3356
|
+
baseUrl: this.validatedServiceOrigin(serviceKey, DEFAULT_APP_URLS[serviceKey])
|
|
3357
|
+
};
|
|
3082
3358
|
}
|
|
3083
3359
|
const live = await this.resolveLiveCallableService(serviceKey);
|
|
3084
3360
|
if (live) {
|
|
3085
3361
|
const overrideUrl = resolveConfiguredServiceUrl(serviceKey, this.serviceUrls);
|
|
3086
|
-
return overrideUrl ? { ...live, baseUrl: overrideUrl } : live;
|
|
3362
|
+
return overrideUrl ? { ...live, baseUrl: this.validatedServiceOrigin(serviceKey, overrideUrl) } : { ...live, baseUrl: this.validatedServiceOrigin(serviceKey, live.baseUrl) };
|
|
3087
3363
|
}
|
|
3088
3364
|
throw new TopoloSdkError(
|
|
3089
3365
|
"unknown_service",
|
|
@@ -3097,10 +3373,10 @@ var TopoloClient = class _TopoloClient {
|
|
|
3097
3373
|
serviceKey,
|
|
3098
3374
|
overrides: this.serviceUrls
|
|
3099
3375
|
});
|
|
3100
|
-
if (baseUrl) return { serviceKey, baseUrl };
|
|
3376
|
+
if (baseUrl) return { serviceKey, baseUrl: this.validatedServiceOrigin(serviceKey, baseUrl) };
|
|
3101
3377
|
const actionPage = await this.searchActions({ service: match.appId, limit: 1 }).catch(() => null);
|
|
3102
3378
|
const actionBaseUrl = actionPage?.actions.find((action) => action.apiBaseUrl)?.apiBaseUrl ?? null;
|
|
3103
|
-
return actionBaseUrl ? { serviceKey, baseUrl:
|
|
3379
|
+
return actionBaseUrl ? { serviceKey, baseUrl: this.validatedServiceOrigin(serviceKey, actionBaseUrl) } : null;
|
|
3104
3380
|
}
|
|
3105
3381
|
/**
|
|
3106
3382
|
* Resolve a service-slug → opaque app-id via caller config or TopoloAuth.
|
|
@@ -3120,7 +3396,7 @@ var TopoloClient = class _TopoloClient {
|
|
|
3120
3396
|
if (!normalized) return null;
|
|
3121
3397
|
const configuredAppId = resolveConfiguredAppId(normalized, this.appIds);
|
|
3122
3398
|
if (configuredAppId) return configuredAppId;
|
|
3123
|
-
const authBase = resolveServiceUrl("developers", this.serviceUrls);
|
|
3399
|
+
const authBase = this.validatedServiceOrigin("developers", resolveServiceUrl("developers", this.serviceUrls));
|
|
3124
3400
|
const cacheKey = `${authBase}|${normalized}`;
|
|
3125
3401
|
const now = Date.now();
|
|
3126
3402
|
const cached = _TopoloClient.appIdCache.get(cacheKey);
|
|
@@ -3296,7 +3572,7 @@ var TopoloClient = class _TopoloClient {
|
|
|
3296
3572
|
"TopoloAuth did not return an action catalog for the current credential."
|
|
3297
3573
|
);
|
|
3298
3574
|
}
|
|
3299
|
-
const entries = raw.map((item) => normalizeActionCatalogEntry(item)).filter((entry) => Boolean(entry));
|
|
3575
|
+
const entries = raw.map((item) => normalizeActionCatalogEntry(item)).filter((entry) => Boolean(entry)).map((entry) => this.validatedActionEntry(entry));
|
|
3300
3576
|
return {
|
|
3301
3577
|
actions: entries,
|
|
3302
3578
|
page: normalizeCatalogPage(
|
|
@@ -3361,7 +3637,7 @@ var TopoloClient = class _TopoloClient {
|
|
|
3361
3637
|
const raw = res.data?.action ?? res.action;
|
|
3362
3638
|
const match = normalizeActionCatalogEntry(raw);
|
|
3363
3639
|
if (!match) throw new TopoloSdkError("unknown_action", `Unknown or unavailable Topolo action "${action}".`);
|
|
3364
|
-
return match;
|
|
3640
|
+
return this.validatedActionEntry(match);
|
|
3365
3641
|
}
|
|
3366
3642
|
async callAction(action, input = {}, options = {}) {
|
|
3367
3643
|
const entry = await this.getAction(action);
|
|
@@ -3438,6 +3714,25 @@ var TopoloClient = class _TopoloClient {
|
|
|
3438
3714
|
}
|
|
3439
3715
|
async planAction(action, input = {}, options = {}) {
|
|
3440
3716
|
const entry = await this.getAction(action);
|
|
3717
|
+
return this.planResolvedAction(entry, input, options);
|
|
3718
|
+
}
|
|
3719
|
+
async prepareAction(action, input, options = {}) {
|
|
3720
|
+
const entry = await this.getAction(action);
|
|
3721
|
+
const example = (entry.agentContract?.examples ?? []).find((candidate) => Boolean(candidate?.input) && typeof candidate.input === "object" && !Array.isArray(candidate.input) && validateTopoloActionInput(entry.inputSchema, candidate.input).valid) ?? null;
|
|
3722
|
+
const selectedInput = input ?? example?.input ?? {};
|
|
3723
|
+
const plan = this.planResolvedAction(entry, selectedInput, options);
|
|
3724
|
+
return {
|
|
3725
|
+
action: entry,
|
|
3726
|
+
example,
|
|
3727
|
+
input: selectedInput,
|
|
3728
|
+
inputSource: input ? "provided" : example ? "example" : "empty",
|
|
3729
|
+
missingFields: [...new Set(
|
|
3730
|
+
plan.validation.errors.filter((issue) => issue.code === "required").map((issue) => issue.path)
|
|
3731
|
+
)],
|
|
3732
|
+
plan
|
|
3733
|
+
};
|
|
3734
|
+
}
|
|
3735
|
+
planResolvedAction(entry, input, options) {
|
|
3441
3736
|
const validation = validateTopoloActionInput(entry.inputSchema, input);
|
|
3442
3737
|
const agentAccess = entry.agentAccess ?? (entry.destructive ? "off" : entry.readOnly && !entry.requiresConfirmation ? "auto" : "confirm");
|
|
3443
3738
|
const confirmationRequired = agentAccess === "confirm" || entry.requiresConfirmation || entry.destructive;
|
|
@@ -3895,7 +4190,16 @@ function pickString(value) {
|
|
|
3895
4190
|
function extractRequired(body) {
|
|
3896
4191
|
if (body && typeof body === "object") {
|
|
3897
4192
|
const maybe = body;
|
|
3898
|
-
if (Array.isArray(maybe.required)) return maybe.required
|
|
4193
|
+
if (Array.isArray(maybe.required)) return canonicalTopoloRequired(maybe.required);
|
|
4194
|
+
for (const nested of [maybe.details, maybe.error]) {
|
|
4195
|
+
if (!nested || typeof nested !== "object" || Array.isArray(nested)) continue;
|
|
4196
|
+
const record = nested;
|
|
4197
|
+
if (Array.isArray(record.required)) return canonicalTopoloRequired(record.required);
|
|
4198
|
+
if (record.details && typeof record.details === "object" && !Array.isArray(record.details)) {
|
|
4199
|
+
const required = record.details.required;
|
|
4200
|
+
if (Array.isArray(required)) return canonicalTopoloRequired(required);
|
|
4201
|
+
}
|
|
4202
|
+
}
|
|
3899
4203
|
}
|
|
3900
4204
|
return [];
|
|
3901
4205
|
}
|
|
@@ -3943,6 +4247,72 @@ function compactActionPlan(plan) {
|
|
|
3943
4247
|
nextActionIds: plan.nextActionIds
|
|
3944
4248
|
};
|
|
3945
4249
|
}
|
|
4250
|
+
function requiredContextText(value, field) {
|
|
4251
|
+
if (typeof value !== "string" || !value.trim() || value.trim().toLowerCase() === "unknown") {
|
|
4252
|
+
throw new TopoloSdkError("invalid_identity_context", `Effective Topolo ${field} is incomplete.`);
|
|
4253
|
+
}
|
|
4254
|
+
return value.trim();
|
|
4255
|
+
}
|
|
4256
|
+
function createTopoloAgentExecutionContext(input) {
|
|
4257
|
+
if (input.contextKey !== null && input.contextKey !== "personal" && !/^org:[^:]+$/.test(input.contextKey)) {
|
|
4258
|
+
throw new TopoloSdkError("invalid_identity_context", "Effective Topolo context key is invalid.");
|
|
4259
|
+
}
|
|
4260
|
+
const user = Object.freeze({
|
|
4261
|
+
id: requiredContextText(input.identity.user.id, "user id"),
|
|
4262
|
+
email: requiredContextText(input.identity.user.email, "user email"),
|
|
4263
|
+
name: input.identity.user.name?.trim() || null,
|
|
4264
|
+
role: requiredContextText(input.identity.user.role, "user role")
|
|
4265
|
+
});
|
|
4266
|
+
const organization = input.identity.organization ? Object.freeze({
|
|
4267
|
+
id: requiredContextText(input.identity.organization.id, "organization id"),
|
|
4268
|
+
slug: requiredContextText(input.identity.organization.slug, "organization slug"),
|
|
4269
|
+
name: requiredContextText(input.identity.organization.name, "organization name")
|
|
4270
|
+
}) : null;
|
|
4271
|
+
if (input.contextKey === "personal" && organization) {
|
|
4272
|
+
throw new TopoloSdkError("identity_context_mismatch", "Personal Topolo context resolved an organization.");
|
|
4273
|
+
}
|
|
4274
|
+
if (input.contextKey?.startsWith("org:")) {
|
|
4275
|
+
const expectedOrganizationId = input.contextKey.slice("org:".length);
|
|
4276
|
+
if (!organization || organization.id !== expectedOrganizationId) {
|
|
4277
|
+
throw new TopoloSdkError("identity_context_mismatch", "Pinned Topolo organization does not match the effective identity.");
|
|
4278
|
+
}
|
|
4279
|
+
}
|
|
4280
|
+
return Object.freeze({
|
|
4281
|
+
environment: input.environment,
|
|
4282
|
+
contextKey: input.contextKey,
|
|
4283
|
+
user,
|
|
4284
|
+
organization,
|
|
4285
|
+
serviceOrigins: Object.freeze(topoloServiceOrigins(input.serviceUrls, input.environment))
|
|
4286
|
+
});
|
|
4287
|
+
}
|
|
4288
|
+
function compactActionPreparationCore(preparation) {
|
|
4289
|
+
const example = preparation.example ? { title: preparation.example.title ?? null, input: preparation.example.input } : null;
|
|
4290
|
+
const effectiveInput = preparation.inputSource === "example" ? { source: "example" } : { source: preparation.inputSource, value: preparation.input };
|
|
4291
|
+
return {
|
|
4292
|
+
action: {
|
|
4293
|
+
id: preparation.action.actionId,
|
|
4294
|
+
title: preparation.action.title,
|
|
4295
|
+
docsUrl: preparation.action.agentContract?.docsUrl ?? null
|
|
4296
|
+
},
|
|
4297
|
+
example,
|
|
4298
|
+
input: effectiveInput,
|
|
4299
|
+
missingFields: preparation.missingFields,
|
|
4300
|
+
valid: preparation.plan.validation.valid,
|
|
4301
|
+
errors: preparation.plan.validation.errors,
|
|
4302
|
+
request: preparation.plan.request,
|
|
4303
|
+
confirmation: {
|
|
4304
|
+
required: preparation.plan.confirmationRequired,
|
|
4305
|
+
provided: preparation.plan.confirmationProvided
|
|
4306
|
+
},
|
|
4307
|
+
executable: preparation.plan.executable,
|
|
4308
|
+
verification: preparation.plan.verification,
|
|
4309
|
+
rollbackActionId: preparation.plan.rollbackActionId
|
|
4310
|
+
};
|
|
4311
|
+
}
|
|
4312
|
+
function compactActionPreparation(preparation, context) {
|
|
4313
|
+
const compact = compactActionPreparationCore(preparation);
|
|
4314
|
+
return context ? { context, ...compact } : compact;
|
|
4315
|
+
}
|
|
3946
4316
|
function compactApplicationAudit(report) {
|
|
3947
4317
|
return {
|
|
3948
4318
|
version: report.version,
|
|
@@ -4081,7 +4451,7 @@ function createTopolo(options) {
|
|
|
4081
4451
|
};
|
|
4082
4452
|
}
|
|
4083
4453
|
|
|
4084
|
-
//
|
|
4454
|
+
// ../../../development-workflow-acceleration/node_modules/.pnpm/@topolo+sdk@0.10.4/node_modules/@topolo/sdk/dist/node/credential-store.js
|
|
4085
4455
|
import { mkdir, readFile, writeFile, chmod } from "fs/promises";
|
|
4086
4456
|
import { existsSync } from "fs";
|
|
4087
4457
|
import { homedir, platform } from "os";
|
|
@@ -4094,6 +4464,90 @@ var TopoloSdkError2 = class extends Error {
|
|
|
4094
4464
|
this.code = code;
|
|
4095
4465
|
}
|
|
4096
4466
|
};
|
|
4467
|
+
var PUBLIC_ERROR_CODES2 = /* @__PURE__ */ new Set([
|
|
4468
|
+
"action_app_identity_unavailable",
|
|
4469
|
+
"action_authentication_required",
|
|
4470
|
+
"action_catalog_summary_unavailable",
|
|
4471
|
+
"action_catalog_unavailable",
|
|
4472
|
+
"action_contract_mismatch",
|
|
4473
|
+
"action_permission_required",
|
|
4474
|
+
"action_runtime_authorization_required",
|
|
4475
|
+
"action_service_not_allowed",
|
|
4476
|
+
"auth_error",
|
|
4477
|
+
"authentication_required",
|
|
4478
|
+
"authorization_pending",
|
|
4479
|
+
"bad_sig",
|
|
4480
|
+
"confirmation_required",
|
|
4481
|
+
"dependency_unavailable",
|
|
4482
|
+
"environment_mismatch",
|
|
4483
|
+
"environment_service_url_required",
|
|
4484
|
+
"expired_token",
|
|
4485
|
+
"forbidden",
|
|
4486
|
+
"http_error",
|
|
4487
|
+
"immutable_drift",
|
|
4488
|
+
"integration_dependency_unavailable",
|
|
4489
|
+
"integration_misconfigured",
|
|
4490
|
+
"invalid_action",
|
|
4491
|
+
"invalid_action_input",
|
|
4492
|
+
"invalid_grant",
|
|
4493
|
+
"invalid_input",
|
|
4494
|
+
"invalid_token",
|
|
4495
|
+
"mcp_error",
|
|
4496
|
+
"membership_required",
|
|
4497
|
+
"network_error",
|
|
4498
|
+
"organization_context_required",
|
|
4499
|
+
"organization_enrollment_required",
|
|
4500
|
+
"permission_denied",
|
|
4501
|
+
"rate_limited",
|
|
4502
|
+
"request_failed",
|
|
4503
|
+
"service_catalog_unavailable",
|
|
4504
|
+
"service_path_origin_mismatch",
|
|
4505
|
+
"service_required",
|
|
4506
|
+
"slow_down",
|
|
4507
|
+
"state_conflict",
|
|
4508
|
+
"unauthorized",
|
|
4509
|
+
"unknown_action",
|
|
4510
|
+
"unknown_service"
|
|
4511
|
+
]);
|
|
4512
|
+
var ERROR_CODE_PATTERN2 = /^[a-z][a-z0-9_]{0,63}$/;
|
|
4513
|
+
var TOPOLO_REQUEST_ID_PATTERN2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
4514
|
+
var CLOUDFLARE_RAY_ID_PATTERN2 = /^[0-9a-f]{16}-[A-Z]{3}$/;
|
|
4515
|
+
var PERMISSION_PATTERN2 = /^[a-z][a-z0-9_-]{0,63}(?:\.[a-z][a-z0-9_-]{0,63})*:(?:[a-z][a-z0-9_-]{0,63}|\*)$/;
|
|
4516
|
+
var RETRY_AFTER_DATE_PATTERN2 = /^(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun), \d{2} (?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) \d{4} \d{2}:\d{2}:\d{2} GMT$/;
|
|
4517
|
+
var PUBLIC_DETAIL_SOURCES2 = /* @__PURE__ */ new Set([
|
|
4518
|
+
"api_key_introspection",
|
|
4519
|
+
"api_key_validation",
|
|
4520
|
+
"auth_context",
|
|
4521
|
+
"auth_validate",
|
|
4522
|
+
"catalog",
|
|
4523
|
+
"credential",
|
|
4524
|
+
"runtime",
|
|
4525
|
+
"service_token_provider",
|
|
4526
|
+
"service_verify_key",
|
|
4527
|
+
"token_validation",
|
|
4528
|
+
"upstream"
|
|
4529
|
+
]);
|
|
4530
|
+
function canonicalTopoloErrorCode2(value) {
|
|
4531
|
+
if (typeof value !== "string") return null;
|
|
4532
|
+
const candidate = value.trim().toLowerCase();
|
|
4533
|
+
if (!ERROR_CODE_PATTERN2.test(candidate) || !PUBLIC_ERROR_CODES2.has(candidate)) return null;
|
|
4534
|
+
return candidate;
|
|
4535
|
+
}
|
|
4536
|
+
function canonicalTopoloRequestId2(value) {
|
|
4537
|
+
if (typeof value !== "string") return void 0;
|
|
4538
|
+
const candidate = value.trim();
|
|
4539
|
+
return TOPOLO_REQUEST_ID_PATTERN2.test(candidate) || CLOUDFLARE_RAY_ID_PATTERN2.test(candidate) ? candidate : void 0;
|
|
4540
|
+
}
|
|
4541
|
+
function canonicalTopoloRetryAfter2(value) {
|
|
4542
|
+
if (typeof value !== "string") return void 0;
|
|
4543
|
+
const candidate = value.trim();
|
|
4544
|
+
if (/^\d{1,10}$/.test(candidate) || RETRY_AFTER_DATE_PATTERN2.test(candidate)) return candidate;
|
|
4545
|
+
return void 0;
|
|
4546
|
+
}
|
|
4547
|
+
function canonicalTopoloRequired2(value) {
|
|
4548
|
+
if (!Array.isArray(value)) return [];
|
|
4549
|
+
return [...new Set(value.filter((item) => typeof item === "string" && PERMISSION_PATTERN2.test(item)))].slice(0, 32);
|
|
4550
|
+
}
|
|
4097
4551
|
var TopoloAuthError2 = class extends TopoloSdkError2 {
|
|
4098
4552
|
status;
|
|
4099
4553
|
body;
|
|
@@ -4101,27 +4555,91 @@ var TopoloAuthError2 = class extends TopoloSdkError2 {
|
|
|
4101
4555
|
path;
|
|
4102
4556
|
requestId;
|
|
4103
4557
|
retryAfter;
|
|
4558
|
+
details;
|
|
4104
4559
|
constructor(message, code = "auth_error", options = {}) {
|
|
4105
|
-
super(code, message);
|
|
4560
|
+
super(canonicalTopoloErrorCode2(code) ?? "auth_error", message);
|
|
4106
4561
|
this.name = "TopoloAuthError";
|
|
4107
4562
|
this.status = options.status ?? 401;
|
|
4108
4563
|
this.body = options.body;
|
|
4109
4564
|
this.service = options.service;
|
|
4110
4565
|
this.path = options.path;
|
|
4111
|
-
this.requestId = options.requestId;
|
|
4112
|
-
this.retryAfter = options.retryAfter;
|
|
4566
|
+
this.requestId = canonicalTopoloRequestId2(options.requestId);
|
|
4567
|
+
this.retryAfter = canonicalTopoloRetryAfter2(options.retryAfter);
|
|
4568
|
+
this.details = extractTopoloServiceErrorDetails2(
|
|
4569
|
+
options.details === void 0 ? options.body : { details: options.details }
|
|
4570
|
+
);
|
|
4113
4571
|
}
|
|
4114
4572
|
};
|
|
4115
|
-
function
|
|
4116
|
-
if (!body || typeof body !== "object" || Array.isArray(body)) return
|
|
4573
|
+
function extractTopoloServiceErrorCode2(body) {
|
|
4574
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) return null;
|
|
4117
4575
|
const record = body;
|
|
4118
|
-
|
|
4576
|
+
const topLevel = canonicalTopoloErrorCode2(record.code);
|
|
4577
|
+
if (topLevel) return topLevel;
|
|
4119
4578
|
const nested = record.error;
|
|
4579
|
+
const nestedString = canonicalTopoloErrorCode2(nested);
|
|
4580
|
+
if (nestedString) return nestedString;
|
|
4120
4581
|
if (nested && typeof nested === "object" && !Array.isArray(nested)) {
|
|
4121
4582
|
const code = nested.code;
|
|
4122
|
-
|
|
4583
|
+
const nestedCode = canonicalTopoloErrorCode2(code);
|
|
4584
|
+
if (nestedCode) return nestedCode;
|
|
4123
4585
|
}
|
|
4124
|
-
return
|
|
4586
|
+
return null;
|
|
4587
|
+
}
|
|
4588
|
+
function serviceErrorCode2(body, fallback) {
|
|
4589
|
+
return extractTopoloServiceErrorCode2(body) ?? fallback;
|
|
4590
|
+
}
|
|
4591
|
+
function extractTopoloServiceErrorDetails2(body) {
|
|
4592
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) return void 0;
|
|
4593
|
+
const record = body;
|
|
4594
|
+
let candidate = record.details;
|
|
4595
|
+
const nested = record.error;
|
|
4596
|
+
if (nested && typeof nested === "object" && !Array.isArray(nested)) {
|
|
4597
|
+
const nestedRecord = nested;
|
|
4598
|
+
if (candidate === void 0) candidate = nestedRecord.details ?? nestedRecord;
|
|
4599
|
+
}
|
|
4600
|
+
if (candidate === void 0) candidate = record;
|
|
4601
|
+
if (!candidate || typeof candidate !== "object" || Array.isArray(candidate)) return void 0;
|
|
4602
|
+
const source = candidate;
|
|
4603
|
+
const safe = {};
|
|
4604
|
+
for (const [key, value] of Object.entries(source)) {
|
|
4605
|
+
if (/present$/i.test(key) && typeof value === "boolean") {
|
|
4606
|
+
safe[key] = value;
|
|
4607
|
+
continue;
|
|
4608
|
+
}
|
|
4609
|
+
if (key === "retryable" && typeof value === "boolean") {
|
|
4610
|
+
safe.retryable = value;
|
|
4611
|
+
continue;
|
|
4612
|
+
}
|
|
4613
|
+
if (key === "code") {
|
|
4614
|
+
const sanitized = canonicalTopoloErrorCode2(value);
|
|
4615
|
+
if (sanitized) safe.code = sanitized;
|
|
4616
|
+
continue;
|
|
4617
|
+
}
|
|
4618
|
+
if (key === "source") {
|
|
4619
|
+
const sanitized = typeof value === "string" ? value.trim().toLowerCase() : "";
|
|
4620
|
+
if (PUBLIC_DETAIL_SOURCES2.has(sanitized)) safe.source = sanitized;
|
|
4621
|
+
continue;
|
|
4622
|
+
}
|
|
4623
|
+
if (key === "dependency" || key === "platform") {
|
|
4624
|
+
safe[`${key}Present`] = value !== null && value !== void 0;
|
|
4625
|
+
continue;
|
|
4626
|
+
}
|
|
4627
|
+
if (key === "error" && !safe.code) {
|
|
4628
|
+
const sanitized = canonicalTopoloErrorCode2(value);
|
|
4629
|
+
if (sanitized) safe.code = sanitized;
|
|
4630
|
+
continue;
|
|
4631
|
+
}
|
|
4632
|
+
if (key === "required" && Array.isArray(value)) {
|
|
4633
|
+
const required = canonicalTopoloRequired2(value);
|
|
4634
|
+
if (required.length) safe.required = required;
|
|
4635
|
+
continue;
|
|
4636
|
+
}
|
|
4637
|
+
if (["missingFields", "fields", "paths"].includes(key) && Array.isArray(value)) {
|
|
4638
|
+
const paths = value.filter((item) => typeof item === "string" && /^[$.a-z0-9_[\]-]{1,160}$/i.test(item));
|
|
4639
|
+
if (paths.length) safe[key] = paths;
|
|
4640
|
+
}
|
|
4641
|
+
}
|
|
4642
|
+
return Object.keys(safe).length ? safe : void 0;
|
|
4125
4643
|
}
|
|
4126
4644
|
var TopoloHttpError2 = class extends TopoloSdkError2 {
|
|
4127
4645
|
status;
|
|
@@ -4130,15 +4648,19 @@ var TopoloHttpError2 = class extends TopoloSdkError2 {
|
|
|
4130
4648
|
path;
|
|
4131
4649
|
requestId;
|
|
4132
4650
|
retryAfter;
|
|
4651
|
+
details;
|
|
4133
4652
|
constructor(service, path, status, body, message, options = {}) {
|
|
4134
|
-
super(
|
|
4653
|
+
super(serviceErrorCode2(body, "http_error"), message ?? `HTTP ${status} from ${service}${path}`);
|
|
4135
4654
|
this.name = "TopoloHttpError";
|
|
4136
4655
|
this.status = status;
|
|
4137
4656
|
this.body = body;
|
|
4138
4657
|
this.service = service;
|
|
4139
4658
|
this.path = path;
|
|
4140
|
-
this.requestId = options.requestId;
|
|
4141
|
-
this.retryAfter = options.retryAfter;
|
|
4659
|
+
this.requestId = canonicalTopoloRequestId2(options.requestId);
|
|
4660
|
+
this.retryAfter = canonicalTopoloRetryAfter2(options.retryAfter);
|
|
4661
|
+
this.details = extractTopoloServiceErrorDetails2(
|
|
4662
|
+
options.details === void 0 ? body : { details: options.details }
|
|
4663
|
+
);
|
|
4142
4664
|
}
|
|
4143
4665
|
};
|
|
4144
4666
|
var APP_API_URLS2 = {
|
|
@@ -4244,8 +4766,9 @@ var TopoloOAuth = class {
|
|
|
4244
4766
|
return parsed;
|
|
4245
4767
|
}
|
|
4246
4768
|
};
|
|
4247
|
-
var
|
|
4769
|
+
var TOPOLO_ENVIRONMENT_SERVICE_URLS = {
|
|
4248
4770
|
production: {
|
|
4771
|
+
auth: "https://auth.topolo.app",
|
|
4249
4772
|
developers: "https://developers.topolo.app"
|
|
4250
4773
|
},
|
|
4251
4774
|
staging: {
|
|
@@ -4259,6 +4782,52 @@ var ENVIRONMENT_DEFAULT_SERVICE_URLS = {
|
|
|
4259
4782
|
},
|
|
4260
4783
|
local: {}
|
|
4261
4784
|
};
|
|
4785
|
+
function hostnameHasSuffix2(hostname, suffix) {
|
|
4786
|
+
return hostname === suffix || hostname.endsWith(`.${suffix}`);
|
|
4787
|
+
}
|
|
4788
|
+
function workersDevAccountMatches2(hostname, account) {
|
|
4789
|
+
if (!hostname.endsWith(".workers.dev")) return false;
|
|
4790
|
+
const labels = hostname.slice(0, -".workers.dev".length).split(".");
|
|
4791
|
+
return labels.length >= 2 && labels.at(-1) === account;
|
|
4792
|
+
}
|
|
4793
|
+
function originMatchesEnvironment2(url, environment) {
|
|
4794
|
+
const hostname = url.hostname.toLowerCase();
|
|
4795
|
+
if (environment === "local") {
|
|
4796
|
+
return hostname === "localhost" || hostname.endsWith(".localhost") || hostname === "127.0.0.1" || hostname === "[::1]";
|
|
4797
|
+
}
|
|
4798
|
+
if (url.protocol !== "https:") return false;
|
|
4799
|
+
if (environment === "production") {
|
|
4800
|
+
return hostnameHasSuffix2(hostname, "topolo.app") || hostnameHasSuffix2(hostname, "topolo.io") || workersDevAccountMatches2(hostname, "topolo");
|
|
4801
|
+
}
|
|
4802
|
+
if (environment === "staging") {
|
|
4803
|
+
return hostnameHasSuffix2(hostname, "stg.topolo.us") || workersDevAccountMatches2(hostname, "topolo-staging");
|
|
4804
|
+
}
|
|
4805
|
+
return hostnameHasSuffix2(hostname, "topolo.dev") || workersDevAccountMatches2(hostname, "topolo-development");
|
|
4806
|
+
}
|
|
4807
|
+
function assertTopoloServiceUrlEnvironment2(service, value, environment) {
|
|
4808
|
+
let url;
|
|
4809
|
+
try {
|
|
4810
|
+
url = new URL(value);
|
|
4811
|
+
} catch {
|
|
4812
|
+
throw new TopoloSdkError2("invalid_service_url", `Invalid URL configured for Topolo service "${service}".`);
|
|
4813
|
+
}
|
|
4814
|
+
if (url.protocol !== "https:" && url.protocol !== "http:" || url.username || url.password || !originMatchesEnvironment2(url, environment)) {
|
|
4815
|
+
throw new TopoloSdkError2(
|
|
4816
|
+
"environment_mismatch",
|
|
4817
|
+
`Refusing Topolo service "${service}": ${url.origin} does not belong to ${environment}.`
|
|
4818
|
+
);
|
|
4819
|
+
}
|
|
4820
|
+
return url.origin;
|
|
4821
|
+
}
|
|
4822
|
+
function topoloServiceOrigins2(serviceUrls, environment) {
|
|
4823
|
+
return Object.fromEntries(
|
|
4824
|
+
Object.entries(serviceUrls ?? {}).sort(([left], [right]) => left.localeCompare(right)).map(([service, value]) => [
|
|
4825
|
+
service,
|
|
4826
|
+
assertTopoloServiceUrlEnvironment2(service, value, environment)
|
|
4827
|
+
])
|
|
4828
|
+
);
|
|
4829
|
+
}
|
|
4830
|
+
var ENVIRONMENT_DEFAULT_SERVICE_URLS = TOPOLO_ENVIRONMENT_SERVICE_URLS;
|
|
4262
4831
|
function configPath() {
|
|
4263
4832
|
const override = process.env.TOPOLO_CONFIG_PATH;
|
|
4264
4833
|
if (override) return override;
|
|
@@ -4339,9 +4908,9 @@ function writeProfileConfig(config, environment, profile) {
|
|
|
4339
4908
|
}
|
|
4340
4909
|
async function resolveCredential(environment = selectedEnvironment()) {
|
|
4341
4910
|
const prefix = envVarPrefix(environment);
|
|
4342
|
-
const apiKey = process.env[`${prefix}_API_KEY`] ?? process.env.TOPOLO_API_KEY;
|
|
4911
|
+
const apiKey = process.env[`${prefix}_API_KEY`] ?? (environment === "production" ? process.env.TOPOLO_API_KEY : void 0);
|
|
4343
4912
|
if (apiKey) return { kind: "api_key", apiKey };
|
|
4344
|
-
const accessToken = process.env[`${prefix}_ACCESS_TOKEN`] ?? process.env.TOPOLO_ACCESS_TOKEN;
|
|
4913
|
+
const accessToken = process.env[`${prefix}_ACCESS_TOKEN`] ?? (environment === "production" ? process.env.TOPOLO_ACCESS_TOKEN : void 0);
|
|
4345
4914
|
if (accessToken) return { kind: "access_token", accessToken };
|
|
4346
4915
|
const config = await readConfig();
|
|
4347
4916
|
return profileForEnvironment(config, environment).credential ?? null;
|
|
@@ -4349,7 +4918,7 @@ async function resolveCredential(environment = selectedEnvironment()) {
|
|
|
4349
4918
|
var REFRESH_SKEW_SECONDS = 60;
|
|
4350
4919
|
async function refreshCredentialIfNeeded(environment = selectedEnvironment(), options = {}) {
|
|
4351
4920
|
const prefix = envVarPrefix(environment);
|
|
4352
|
-
if (process.env[`${prefix}_API_KEY`] || process.env[`${prefix}_ACCESS_TOKEN`] || process.env.TOPOLO_API_KEY || process.env.TOPOLO_ACCESS_TOKEN) {
|
|
4921
|
+
if (process.env[`${prefix}_API_KEY`] || process.env[`${prefix}_ACCESS_TOKEN`] || environment === "production" && process.env.TOPOLO_API_KEY || environment === "production" && process.env.TOPOLO_ACCESS_TOKEN) {
|
|
4353
4922
|
return resolveCredential(environment);
|
|
4354
4923
|
}
|
|
4355
4924
|
const config = await readConfig();
|
|
@@ -4359,9 +4928,16 @@ async function refreshCredentialIfNeeded(environment = selectedEnvironment(), op
|
|
|
4359
4928
|
if (!cred.refreshToken || !profile.oauthClientId) return cred;
|
|
4360
4929
|
const now = Math.floor(Date.now() / 1e3);
|
|
4361
4930
|
if (!options.force && cred.expiresAt && cred.expiresAt - REFRESH_SKEW_SECONDS > now) return cred;
|
|
4931
|
+
const serviceUrls = resolveServiceUrls(profile.serviceUrls, environment);
|
|
4932
|
+
if (environment === "local" && !serviceUrls?.auth) {
|
|
4933
|
+
throw new TopoloSdkError2(
|
|
4934
|
+
"environment_service_url_required",
|
|
4935
|
+
"Local Topolo credential refresh requires an explicit local Auth service origin."
|
|
4936
|
+
);
|
|
4937
|
+
}
|
|
4362
4938
|
try {
|
|
4363
4939
|
const oauth = new TopoloOAuth({
|
|
4364
|
-
serviceUrls
|
|
4940
|
+
serviceUrls
|
|
4365
4941
|
});
|
|
4366
4942
|
const tokens = await oauth.refreshToken({
|
|
4367
4943
|
clientId: profile.oauthClientId,
|
|
@@ -4402,9 +4978,16 @@ async function refreshContextCredentialIfNeeded(environment, key, options = {})
|
|
|
4402
4978
|
if (!credential.refreshToken || !oauthClientId) return credential;
|
|
4403
4979
|
const now = Math.floor(Date.now() / 1e3);
|
|
4404
4980
|
if (!options.force && credential.expiresAt && credential.expiresAt - REFRESH_SKEW_SECONDS > now) return credential;
|
|
4981
|
+
const serviceUrls = resolveServiceUrls(profile.serviceUrls, environment);
|
|
4982
|
+
if (environment === "local" && !serviceUrls?.auth) {
|
|
4983
|
+
throw new TopoloSdkError2(
|
|
4984
|
+
"environment_service_url_required",
|
|
4985
|
+
"Local Topolo context refresh requires an explicit local Auth service origin."
|
|
4986
|
+
);
|
|
4987
|
+
}
|
|
4405
4988
|
try {
|
|
4406
4989
|
const oauth = new TopoloOAuth({
|
|
4407
|
-
serviceUrls
|
|
4990
|
+
serviceUrls
|
|
4408
4991
|
});
|
|
4409
4992
|
const tokens = await oauth.refreshToken({
|
|
4410
4993
|
clientId: oauthClientId,
|
|
@@ -4451,7 +5034,8 @@ function resolveServiceUrls(configUrls, environment = selectedEnvironment()) {
|
|
|
4451
5034
|
merged[k.slice("TOPOLO_SERVICE_URL_".length).toLowerCase()] = v;
|
|
4452
5035
|
}
|
|
4453
5036
|
}
|
|
4454
|
-
|
|
5037
|
+
const origins = topoloServiceOrigins2(merged, environment);
|
|
5038
|
+
return Object.keys(origins).length > 0 ? origins : void 0;
|
|
4455
5039
|
}
|
|
4456
5040
|
|
|
4457
5041
|
// src/server.ts
|
|
@@ -4816,6 +5400,38 @@ var TOOLS = [
|
|
|
4816
5400
|
return args["detail"] === true ? result : compactActionValidation(result.action.actionId, result.validation);
|
|
4817
5401
|
}
|
|
4818
5402
|
},
|
|
5403
|
+
{
|
|
5404
|
+
name: "topolo_prepare_action",
|
|
5405
|
+
title: "Prepare one Topolo action",
|
|
5406
|
+
description: "Fetches one exact live action contract once and returns one valid published example, missing fields, exact request, confirmation state, verification, and rollback without executing it.",
|
|
5407
|
+
requiredScopes: [],
|
|
5408
|
+
destructive: false,
|
|
5409
|
+
inputSchema: {
|
|
5410
|
+
type: "object",
|
|
5411
|
+
properties: {
|
|
5412
|
+
actionId: { type: "string", minLength: 1 },
|
|
5413
|
+
input: { type: "object", additionalProperties: true },
|
|
5414
|
+
resourceType: { type: "string" },
|
|
5415
|
+
resourceId: { type: "string" },
|
|
5416
|
+
confirm: { type: "boolean", description: "Include confirmation in the plan without executing the action." }
|
|
5417
|
+
},
|
|
5418
|
+
required: ["actionId"],
|
|
5419
|
+
additionalProperties: false
|
|
5420
|
+
},
|
|
5421
|
+
handler: async (topolo, args) => {
|
|
5422
|
+
const actionId = requiredString(args, "actionId");
|
|
5423
|
+
const resource = optionalResourceContext(args);
|
|
5424
|
+
const preparation = await topolo.client.prepareAction(
|
|
5425
|
+
actionId,
|
|
5426
|
+
args["input"] === void 0 ? void 0 : coerceObjectParam(args["input"], "input"),
|
|
5427
|
+
{
|
|
5428
|
+
confirm: args["confirm"] === true,
|
|
5429
|
+
...resource ? { resource } : {}
|
|
5430
|
+
}
|
|
5431
|
+
);
|
|
5432
|
+
return compactActionPreparation(preparation);
|
|
5433
|
+
}
|
|
5434
|
+
},
|
|
4819
5435
|
{
|
|
4820
5436
|
name: "topolo_plan_action",
|
|
4821
5437
|
title: "Plan one Topolo action",
|
|
@@ -5283,11 +5899,29 @@ function readMcpVersion() {
|
|
|
5283
5899
|
}
|
|
5284
5900
|
var MCP_VERSION = readMcpVersion();
|
|
5285
5901
|
|
|
5902
|
+
// src/context.ts
|
|
5903
|
+
function createMcpExecutionContext(input) {
|
|
5904
|
+
return createTopoloAgentExecutionContext(input);
|
|
5905
|
+
}
|
|
5906
|
+
function mcpServerInstructions(context) {
|
|
5907
|
+
return JSON.stringify({
|
|
5908
|
+
effectiveTopoloContext: context,
|
|
5909
|
+
rules: [
|
|
5910
|
+
"Use only this environment, identity, organization, context key, and service origins.",
|
|
5911
|
+
"Prepare mutations before execution and preserve the unchanged request for confirmation.",
|
|
5912
|
+
"Never infer undisclosed actions or switch organization context inside this MCP session."
|
|
5913
|
+
]
|
|
5914
|
+
});
|
|
5915
|
+
}
|
|
5916
|
+
function mcpResultEnvelope(context, data) {
|
|
5917
|
+
return { context, data };
|
|
5918
|
+
}
|
|
5919
|
+
|
|
5286
5920
|
// src/server.ts
|
|
5287
|
-
function buildServer(topolo, scopes, tools = TOOLS) {
|
|
5921
|
+
function buildServer(topolo, scopes, context, tools = TOOLS) {
|
|
5288
5922
|
const server = new Server(
|
|
5289
5923
|
{ name: "topolo", version: MCP_VERSION },
|
|
5290
|
-
{ capabilities: { tools: {} } }
|
|
5924
|
+
{ capabilities: { tools: {} }, instructions: mcpServerInstructions(context) }
|
|
5291
5925
|
);
|
|
5292
5926
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
5293
5927
|
const availableTools = await buildTopoloTools(topolo, scopes, tools);
|
|
@@ -5310,66 +5944,74 @@ function buildServer(topolo, scopes, tools = TOOLS) {
|
|
|
5310
5944
|
const tool = availableTools.find((t) => t.name === params.name);
|
|
5311
5945
|
if (!tool) {
|
|
5312
5946
|
return toolError(
|
|
5947
|
+
context,
|
|
5313
5948
|
`Tool "${params.name}" is not available for this credential. Either the tool does not exist or the current scopes do not permit it.`
|
|
5314
5949
|
);
|
|
5315
5950
|
}
|
|
5316
5951
|
if (!hasAnyScope(scopes, tool.requiredScopes)) {
|
|
5317
5952
|
return toolError(
|
|
5953
|
+
context,
|
|
5318
5954
|
`Tool "${tool.name}" requires one of: ${tool.requiredScopes.join(", ")}. Current credential does not grant any of these.`
|
|
5319
5955
|
);
|
|
5320
5956
|
}
|
|
5321
5957
|
try {
|
|
5322
5958
|
const result = await tool.handler(topolo, params.arguments ?? {});
|
|
5323
5959
|
return {
|
|
5324
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
5960
|
+
content: [{ type: "text", text: JSON.stringify(mcpResultEnvelope(context, result)) }]
|
|
5325
5961
|
};
|
|
5326
5962
|
} catch (err) {
|
|
5327
5963
|
if (err instanceof TopoloPermissionError) {
|
|
5328
|
-
return toolStructuredError(err,
|
|
5964
|
+
return toolStructuredError(context, err, err.message || "Permission denied.", {
|
|
5329
5965
|
required: err.required
|
|
5330
5966
|
});
|
|
5331
5967
|
}
|
|
5332
5968
|
if (err instanceof TopoloAuthError) {
|
|
5333
|
-
return toolStructuredError(err, "Authentication failed.");
|
|
5969
|
+
return toolStructuredError(context, err, "Authentication failed.");
|
|
5334
5970
|
}
|
|
5335
5971
|
if (err instanceof TopoloHttpError) {
|
|
5336
|
-
return toolStructuredError(err, err.message);
|
|
5972
|
+
return toolStructuredError(context, err, err.message);
|
|
5337
5973
|
}
|
|
5338
|
-
return toolError(publicMcpErrorMessage(err, "Tool execution failed."));
|
|
5974
|
+
return toolError(context, publicMcpErrorMessage(err, "Tool execution failed."));
|
|
5339
5975
|
}
|
|
5340
5976
|
});
|
|
5341
5977
|
return server;
|
|
5342
5978
|
}
|
|
5343
|
-
function toolStructuredError(error,
|
|
5344
|
-
return toolError(
|
|
5345
|
-
|
|
5346
|
-
|
|
5347
|
-
|
|
5348
|
-
|
|
5349
|
-
|
|
5350
|
-
|
|
5351
|
-
|
|
5352
|
-
|
|
5353
|
-
|
|
5354
|
-
|
|
5355
|
-
|
|
5356
|
-
}, null, 2));
|
|
5979
|
+
function toolStructuredError(context, error, _message, extra = {}) {
|
|
5980
|
+
return toolError(context, {
|
|
5981
|
+
code: error.code,
|
|
5982
|
+
status: error.status,
|
|
5983
|
+
message: publicTopoloErrorMessage(error),
|
|
5984
|
+
service: error.service,
|
|
5985
|
+
path: error.path,
|
|
5986
|
+
requestId: error.requestId,
|
|
5987
|
+
retryAfter: error.retryAfter,
|
|
5988
|
+
details: error.details,
|
|
5989
|
+
hint: topoloErrorHint(error),
|
|
5990
|
+
...extra
|
|
5991
|
+
});
|
|
5357
5992
|
}
|
|
5358
5993
|
async function advertisedToolNames(topolo, scopes, tools = TOOLS) {
|
|
5359
5994
|
return (await buildTopoloTools(topolo, scopes, tools)).map((t) => t.name);
|
|
5360
5995
|
}
|
|
5361
|
-
function toolError(
|
|
5996
|
+
function toolError(context, error) {
|
|
5362
5997
|
return {
|
|
5363
5998
|
isError: true,
|
|
5364
|
-
content: [{
|
|
5999
|
+
content: [{
|
|
6000
|
+
type: "text",
|
|
6001
|
+
text: JSON.stringify({
|
|
6002
|
+
context,
|
|
6003
|
+
error: typeof error === "string" ? { code: "mcp_error", message: error } : error
|
|
6004
|
+
})
|
|
6005
|
+
}]
|
|
5365
6006
|
};
|
|
5366
6007
|
}
|
|
5367
6008
|
|
|
5368
6009
|
// src/auth.ts
|
|
5369
|
-
function resolveCredentialFromEnv() {
|
|
5370
|
-
const
|
|
6010
|
+
function resolveCredentialFromEnv(environment = "production") {
|
|
6011
|
+
const prefix = environment === "production" ? "TOPOLO_PRODUCTION" : `TOPOLO_${environment.toUpperCase()}`;
|
|
6012
|
+
const apiKey = process.env[`${prefix}_API_KEY`] ?? (environment === "production" ? process.env.TOPOLO_API_KEY : void 0);
|
|
5371
6013
|
if (apiKey) return { kind: "api_key", apiKey };
|
|
5372
|
-
const accessToken = process.env.TOPOLO_ACCESS_TOKEN;
|
|
6014
|
+
const accessToken = process.env[`${prefix}_ACCESS_TOKEN`] ?? (environment === "production" ? process.env.TOPOLO_ACCESS_TOKEN : void 0);
|
|
5373
6015
|
if (accessToken) return { kind: "access_token", accessToken };
|
|
5374
6016
|
return null;
|
|
5375
6017
|
}
|
|
@@ -5401,7 +6043,7 @@ function resolvePinnedContextKey(profile, selector) {
|
|
|
5401
6043
|
throw new TopoloMcpPublicError(`TOPOLO_CONTEXT "${selector}" is not signed in for this environment`);
|
|
5402
6044
|
}
|
|
5403
6045
|
async function resolveMcpCredential(environment, selector = process.env.TOPOLO_CONTEXT, dependencies = DEFAULT_DEPENDENCIES) {
|
|
5404
|
-
const environmentCredential = dependencies.resolveCredentialFromEnv();
|
|
6046
|
+
const environmentCredential = dependencies.resolveCredentialFromEnv(environment);
|
|
5405
6047
|
const requestedSelector = selector?.trim();
|
|
5406
6048
|
if (environmentCredential) {
|
|
5407
6049
|
if (requestedSelector) {
|
|
@@ -5410,15 +6052,16 @@ async function resolveMcpCredential(environment, selector = process.env.TOPOLO_C
|
|
|
5410
6052
|
return { credential: environmentCredential, contextKey: null };
|
|
5411
6053
|
}
|
|
5412
6054
|
if (requestedSelector) {
|
|
5413
|
-
const
|
|
6055
|
+
const config2 = await dependencies.readConfig();
|
|
5414
6056
|
const contextKey = resolvePinnedContextKey(
|
|
5415
|
-
profileForEnvironment(
|
|
6057
|
+
profileForEnvironment(config2, environment),
|
|
5416
6058
|
requestedSelector
|
|
5417
6059
|
);
|
|
5418
6060
|
const credential = await dependencies.refreshContextCredentialIfNeeded(environment, contextKey);
|
|
5419
6061
|
return {
|
|
5420
6062
|
credential,
|
|
5421
6063
|
contextKey,
|
|
6064
|
+
storedContext: profileForEnvironment(config2, environment).contexts?.[contextKey],
|
|
5422
6065
|
credentialProvider: () => dependencies.refreshContextCredentialIfNeeded(
|
|
5423
6066
|
environment,
|
|
5424
6067
|
contextKey
|
|
@@ -5430,6 +6073,25 @@ async function resolveMcpCredential(environment, selector = process.env.TOPOLO_C
|
|
|
5430
6073
|
)
|
|
5431
6074
|
};
|
|
5432
6075
|
}
|
|
6076
|
+
const config = await dependencies.readConfig();
|
|
6077
|
+
const profile = profileForEnvironment(config, environment);
|
|
6078
|
+
const activeContext = profile.activeContext;
|
|
6079
|
+
if (activeContext) {
|
|
6080
|
+
const storedContext = profile.contexts?.[activeContext];
|
|
6081
|
+
if (!storedContext) {
|
|
6082
|
+
throw new TopoloMcpPublicError(`Active Topolo context "${activeContext}" is not signed in for this environment`);
|
|
6083
|
+
}
|
|
6084
|
+
return {
|
|
6085
|
+
credential: await dependencies.refreshContextCredentialIfNeeded(environment, activeContext),
|
|
6086
|
+
contextKey: activeContext,
|
|
6087
|
+
storedContext,
|
|
6088
|
+
credentialProvider: () => dependencies.refreshContextCredentialIfNeeded(environment, activeContext),
|
|
6089
|
+
refreshCredential: () => dependencies.refreshContextCredentialIfNeeded(environment, activeContext, { force: true })
|
|
6090
|
+
};
|
|
6091
|
+
}
|
|
6092
|
+
if (Object.keys(profile.contexts ?? {}).length > 0) {
|
|
6093
|
+
throw new TopoloMcpPublicError("No active Topolo context is selected for this environment");
|
|
6094
|
+
}
|
|
5433
6095
|
return {
|
|
5434
6096
|
credential: await dependencies.refreshCredentialIfNeeded(environment),
|
|
5435
6097
|
contextKey: null,
|
|
@@ -5484,6 +6146,7 @@ async function runStdioMcp(dependencies = DEFAULT_DEPENDENCIES2) {
|
|
|
5484
6146
|
const serviceUrls = dependencies.resolveServiceUrls(profile.serviceUrls, environment);
|
|
5485
6147
|
const topolo = dependencies.createTopolo({
|
|
5486
6148
|
credential,
|
|
6149
|
+
environment,
|
|
5487
6150
|
agent: {
|
|
5488
6151
|
clientName: "topolo-mcp",
|
|
5489
6152
|
clientVersion: MCP_VERSION,
|
|
@@ -5494,8 +6157,15 @@ async function runStdioMcp(dependencies = DEFAULT_DEPENDENCIES2) {
|
|
|
5494
6157
|
...resolvedCredential.credentialProvider ? { credentialProvider: resolvedCredential.credentialProvider } : {},
|
|
5495
6158
|
...resolvedCredential.refreshCredential ? { refreshCredential: resolvedCredential.refreshCredential } : {}
|
|
5496
6159
|
});
|
|
5497
|
-
const
|
|
5498
|
-
const
|
|
6160
|
+
const identity = await loadIdentity(topolo);
|
|
6161
|
+
const scopes = scopesForIdentity(identity);
|
|
6162
|
+
const executionContext = createMcpExecutionContext({
|
|
6163
|
+
environment,
|
|
6164
|
+
contextKey: resolvedCredential.contextKey,
|
|
6165
|
+
identity,
|
|
6166
|
+
serviceUrls
|
|
6167
|
+
});
|
|
6168
|
+
const server = dependencies.buildServer(topolo, scopes, executionContext);
|
|
5499
6169
|
const transport = dependencies.createTransport();
|
|
5500
6170
|
await server.connect(transport);
|
|
5501
6171
|
const toolNames = await dependencies.advertisedToolNames(topolo, scopes);
|
|
@@ -5505,14 +6175,19 @@ async function runStdioMcp(dependencies = DEFAULT_DEPENDENCIES2) {
|
|
|
5505
6175
|
);
|
|
5506
6176
|
}
|
|
5507
6177
|
async function loadScopes(topolo) {
|
|
6178
|
+
return scopesForIdentity(await loadIdentity(topolo));
|
|
6179
|
+
}
|
|
6180
|
+
function scopesForIdentity(info) {
|
|
6181
|
+
const userWithOrgSlug = info.user;
|
|
6182
|
+
return {
|
|
6183
|
+
permissions: info.user.permissions,
|
|
6184
|
+
role: info.user.role,
|
|
6185
|
+
orgSlug: info.organization?.slug || userWithOrgSlug.orgSlug || void 0
|
|
6186
|
+
};
|
|
6187
|
+
}
|
|
6188
|
+
async function loadIdentity(topolo) {
|
|
5508
6189
|
try {
|
|
5509
|
-
|
|
5510
|
-
const userWithOrgSlug = info.user;
|
|
5511
|
-
return {
|
|
5512
|
-
permissions: info.user.permissions,
|
|
5513
|
-
role: info.user.role,
|
|
5514
|
-
orgSlug: info.organization?.slug || userWithOrgSlug.orgSlug || void 0
|
|
5515
|
-
};
|
|
6190
|
+
return await topolo.identity.whoami();
|
|
5516
6191
|
} catch (err) {
|
|
5517
6192
|
if (err instanceof TopoloAuthError) {
|
|
5518
6193
|
throw new TopoloMcpPublicError("credential rejected.");
|
|
@@ -5544,9 +6219,11 @@ export {
|
|
|
5544
6219
|
createStdioTransport,
|
|
5545
6220
|
exitStdioProcess,
|
|
5546
6221
|
isDirectExecution,
|
|
6222
|
+
loadIdentity,
|
|
5547
6223
|
loadScopes,
|
|
5548
6224
|
readStdioEnvironment,
|
|
5549
6225
|
runStdioMcp,
|
|
6226
|
+
scopesForIdentity,
|
|
5550
6227
|
terminateStdioMcp,
|
|
5551
6228
|
writeStdioError
|
|
5552
6229
|
};
|