@topolo/mcp 0.12.0 → 0.13.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/README.md +33 -15
- package/dist/index.js +403 -43
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -24,16 +24,29 @@ Customers install only `@topolo/cli`; it owns an exact compatible MCP and SDK.
|
|
|
24
24
|
`topolo setup` registers the resolved local Node entry point, so agent startup
|
|
25
25
|
does not depend on `npx`, network access, or a separately synchronized package.
|
|
26
26
|
|
|
27
|
-
##
|
|
27
|
+
## Register an accountable agent installation
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
For Codex, Claude, and other persistent agent hosts, register the host once
|
|
30
|
+
from an organization context:
|
|
30
31
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
32
|
+
```bash
|
|
33
|
+
topolo installations register \
|
|
34
|
+
--name "Codex on my workstation" \
|
|
35
|
+
--provider openai \
|
|
36
|
+
--host codex \
|
|
37
|
+
--grant app_topolo_agent:actions
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
The CLI creates an accountable external principal, generates a device-local
|
|
41
|
+
DPoP key, stores the client secret and private key in the operating system's
|
|
42
|
+
protected credential store, and configures the host with only
|
|
43
|
+
`TOPOLO_AGENT_INSTALLATION_ID`. The MCP exchanges short-lived credentials for
|
|
44
|
+
the exact service, task, and resource being accessed. No reusable secret is
|
|
45
|
+
written to Codex or Claude configuration.
|
|
46
|
+
|
|
47
|
+
Human OAuth contexts and explicit API keys remain supported for interactive
|
|
48
|
+
and compatibility use, but they are never used as a fallback when an
|
|
49
|
+
installation ID is configured.
|
|
37
50
|
|
|
38
51
|
## Register with an MCP client
|
|
39
52
|
|
|
@@ -50,11 +63,13 @@ must not use an `npx` launcher because that makes startup network-dependent.
|
|
|
50
63
|
| `TOPOLO_ACCESS_TOKEN` | Short-lived JWT (dev/testing) |
|
|
51
64
|
| `TOPOLO_ENV` | Credential environment (production by default) |
|
|
52
65
|
| `TOPOLO_CONTEXT` | Pin one stored personal/org context without activating it |
|
|
66
|
+
| `TOPOLO_AGENT_INSTALLATION_ID` | Use a protected accountable agent installation |
|
|
67
|
+
| `TOPOLO_AGENT_TASK_ID` | Optional stable host task identifier |
|
|
53
68
|
| `TOPOLO_AGENT_NAME` | Human-readable agent label for audit logs |
|
|
54
69
|
| `TOPOLO_SERVICE_URL_<ID>` | Override a service base URL (e.g. `_AUTH`, `_MAIL`) |
|
|
55
70
|
|
|
56
|
-
|
|
57
|
-
|
|
71
|
+
An installation ID cannot be combined with a human context, API key, or access
|
|
72
|
+
token. The MCP rejects ambiguous configuration instead of choosing one.
|
|
58
73
|
|
|
59
74
|
For concurrent organization or environment operation, run one named MCP server
|
|
60
75
|
per stored context. `TOPOLO_CONTEXT` accepts an exact key from `topolo context
|
|
@@ -83,11 +98,12 @@ credentials already carry their own immutable context.
|
|
|
83
98
|
|
|
84
99
|
## Startup sequence
|
|
85
100
|
|
|
86
|
-
1. Resolve
|
|
87
|
-
active stored credential. **Refuse to start**
|
|
101
|
+
1. Resolve a protected installation, injected credential, pinned stored
|
|
102
|
+
context, or the environment's active stored credential. **Refuse to start**
|
|
103
|
+
if none is available. Installation mode exchanges a short-lived Auth token
|
|
104
|
+
bound to the MCP task, organization, and local DPoP key.
|
|
88
105
|
2. Call `GET /api/me` to load the user's granted scopes + role.
|
|
89
106
|
3. Connect the stdio transport and begin serving MCP requests.
|
|
90
|
-
|
|
91
107
|
4. Advertise a fixed, bounded discovery/invocation surface. Agents search one
|
|
92
108
|
application at a time and call exact action IDs; the server never expands
|
|
93
109
|
one MCP tool per action or materializes the global catalog.
|
|
@@ -100,8 +116,10 @@ clients surface that as a failed server launch.
|
|
|
100
116
|
|
|
101
117
|
TopoloMCP is a transport adapter and does not persist customer data. It has no
|
|
102
118
|
database, KV, R2, Durable Object, filesystem-write, analytics, or log-storage
|
|
103
|
-
binding. Credentials remain in the caller-owned environment
|
|
104
|
-
credential store,
|
|
119
|
+
binding. Credentials remain in the caller-owned environment, shared CLI
|
|
120
|
+
credential store, or operating-system protected installation store.
|
|
121
|
+
Installation secrets and private keys are never copied into MCP host
|
|
122
|
+
configuration. Every request is authorized by the target Topolo service.
|
|
105
123
|
|
|
106
124
|
Only messages raised by TopoloMCP's own input validators are returned verbatim.
|
|
107
125
|
Authentication, SDK, provider, session-construction, and unexpected runtime
|
package/dist/index.js
CHANGED
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
5
|
|
|
6
|
-
// node_modules/.pnpm/@topolo+sdk@0.
|
|
6
|
+
// node_modules/.pnpm/@topolo+sdk@0.21.1/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);
|
|
10
10
|
return;
|
|
11
11
|
}
|
|
12
|
-
headers.set("Authorization",
|
|
12
|
+
headers.set("Authorization", `${credential.kind === "dpop_access_token" ? "DPoP" : "Bearer"} ${credential.accessToken}`);
|
|
13
13
|
}
|
|
14
14
|
function applyAuditHeaders(headers, agent, requestId) {
|
|
15
15
|
headers.set("X-Topolo-Client", `${agent.clientName}/${agent.clientVersion}`);
|
|
@@ -22,6 +22,54 @@ function generateRequestId() {
|
|
|
22
22
|
}
|
|
23
23
|
return `req_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
|
|
24
24
|
}
|
|
25
|
+
var encoder = new TextEncoder();
|
|
26
|
+
function encodeBase64Url(value) {
|
|
27
|
+
const bytes = value instanceof Uint8Array ? value : new Uint8Array(value);
|
|
28
|
+
let binary = "";
|
|
29
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
30
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
|
|
31
|
+
}
|
|
32
|
+
function encodeJson(value) {
|
|
33
|
+
return encodeBase64Url(encoder.encode(JSON.stringify(value)));
|
|
34
|
+
}
|
|
35
|
+
async function sha256Base64Url(value) {
|
|
36
|
+
return encodeBase64Url(await crypto.subtle.digest("SHA-256", encoder.encode(value)));
|
|
37
|
+
}
|
|
38
|
+
function publicJwk(privateJwk) {
|
|
39
|
+
const { crv, kty, x, y } = privateJwk;
|
|
40
|
+
if (kty !== "EC" || crv !== "P-256" || !x || !y || !privateJwk.d) {
|
|
41
|
+
throw new TypeError("A private P-256 JSON Web Key is required for DPoP signing.");
|
|
42
|
+
}
|
|
43
|
+
return { crv, kty, x, y };
|
|
44
|
+
}
|
|
45
|
+
function canonicalTargetUrl(value) {
|
|
46
|
+
const url = new URL(value);
|
|
47
|
+
return `${url.origin}${url.pathname}`;
|
|
48
|
+
}
|
|
49
|
+
async function createDpopProof(input) {
|
|
50
|
+
const jwk = publicJwk(input.privateJwk);
|
|
51
|
+
const key = await crypto.subtle.importKey(
|
|
52
|
+
"jwk",
|
|
53
|
+
input.privateJwk,
|
|
54
|
+
{ name: "ECDSA", namedCurve: "P-256" },
|
|
55
|
+
false,
|
|
56
|
+
["sign"]
|
|
57
|
+
);
|
|
58
|
+
const header = encodeJson({ alg: "ES256", typ: "dpop+jwt", jwk });
|
|
59
|
+
const claims = encodeJson({
|
|
60
|
+
jti: crypto.randomUUID(),
|
|
61
|
+
iat: Math.floor(Date.now() / 1e3),
|
|
62
|
+
htm: input.method.toUpperCase(),
|
|
63
|
+
htu: canonicalTargetUrl(input.targetUrl),
|
|
64
|
+
ath: await sha256Base64Url(input.accessToken)
|
|
65
|
+
});
|
|
66
|
+
const signature = await crypto.subtle.sign(
|
|
67
|
+
{ name: "ECDSA", hash: "SHA-256" },
|
|
68
|
+
key,
|
|
69
|
+
encoder.encode(`${header}.${claims}`)
|
|
70
|
+
);
|
|
71
|
+
return `${header}.${claims}.${encodeBase64Url(signature)}`;
|
|
72
|
+
}
|
|
25
73
|
var TopoloSdkError = class extends Error {
|
|
26
74
|
code;
|
|
27
75
|
constructor(code, message) {
|
|
@@ -1140,7 +1188,7 @@ var TopoloClient = class _TopoloClient {
|
|
|
1140
1188
|
timeoutMs;
|
|
1141
1189
|
fetchImpl;
|
|
1142
1190
|
credentialProvider;
|
|
1143
|
-
|
|
1191
|
+
credentialLoadsInFlight = /* @__PURE__ */ new Map();
|
|
1144
1192
|
refreshCredential;
|
|
1145
1193
|
refreshInFlight = null;
|
|
1146
1194
|
debug;
|
|
@@ -1197,7 +1245,16 @@ var TopoloClient = class _TopoloClient {
|
|
|
1197
1245
|
`Mutating ${method} requests require { confirm: true }. This guardrail prevents agents from issuing writes without an explicit human-in-the-loop acknowledgement.`
|
|
1198
1246
|
);
|
|
1199
1247
|
}
|
|
1200
|
-
|
|
1248
|
+
const actionId = requestActionId(opts.headers);
|
|
1249
|
+
const taskId = opts.actorEnvelope?.delegation?.taskId ?? this.actorContext?.taskId;
|
|
1250
|
+
let requestCredential = await this.loadCurrentCredential({
|
|
1251
|
+
service: normalizeLookupKey(opts.service),
|
|
1252
|
+
method,
|
|
1253
|
+
path: opts.path,
|
|
1254
|
+
...actionId ? { actionId } : {},
|
|
1255
|
+
...taskId ? { taskId } : {},
|
|
1256
|
+
...opts.resource ? { resource: opts.resource } : {}
|
|
1257
|
+
});
|
|
1201
1258
|
const resolvedService = opts.baseUrl ? { serviceKey: normalizeLookupKey(opts.service), baseUrl: this.validatedServiceOrigin(opts.service, opts.baseUrl) } : await this.resolveRequestService(opts.service);
|
|
1202
1259
|
const baseUrl = resolvedService.baseUrl;
|
|
1203
1260
|
const url = new URL(opts.path, baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`);
|
|
@@ -1220,7 +1277,6 @@ var TopoloClient = class _TopoloClient {
|
|
|
1220
1277
|
if (opts.body !== void 0) headers.set("Content-Type", "application/json");
|
|
1221
1278
|
const platformAppId = resolveConfiguredAppId(resolvedService.serviceKey, this.appIds);
|
|
1222
1279
|
if (platformAppId) headers.set("X-App-ID", platformAppId);
|
|
1223
|
-
applyAuthHeaders(headers, this.credential);
|
|
1224
1280
|
const actorEnvelope = opts.actorEnvelope === void 0 ? void 0 : parseActorEnvelope(opts.actorEnvelope);
|
|
1225
1281
|
const requestId = actorEnvelope?.action.requestId ?? generateRequestId();
|
|
1226
1282
|
applyAuditHeaders(headers, this.agent, requestId);
|
|
@@ -1254,9 +1310,19 @@ var TopoloClient = class _TopoloClient {
|
|
|
1254
1310
|
}, this.timeoutMs);
|
|
1255
1311
|
const signal = opts.signal ? mergeSignals(opts.signal, controller.signal) : controller.signal;
|
|
1256
1312
|
try {
|
|
1313
|
+
const requestHeaders = new Headers(headers);
|
|
1314
|
+
applyAuthHeaders(requestHeaders, requestCredential);
|
|
1315
|
+
if (requestCredential.kind === "dpop_access_token") {
|
|
1316
|
+
requestHeaders.set("DPoP", await createDpopProof({
|
|
1317
|
+
accessToken: requestCredential.accessToken,
|
|
1318
|
+
method,
|
|
1319
|
+
targetUrl: urlStr,
|
|
1320
|
+
privateJwk: requestCredential.privateJwk
|
|
1321
|
+
}));
|
|
1322
|
+
}
|
|
1257
1323
|
return await this.fetchImpl(urlStr, {
|
|
1258
1324
|
method,
|
|
1259
|
-
headers:
|
|
1325
|
+
headers: requestHeaders,
|
|
1260
1326
|
body: opts.rawBody ?? (opts.body !== void 0 ? JSON.stringify(opts.body) : null),
|
|
1261
1327
|
signal
|
|
1262
1328
|
});
|
|
@@ -1287,13 +1353,13 @@ var TopoloClient = class _TopoloClient {
|
|
|
1287
1353
|
};
|
|
1288
1354
|
let res;
|
|
1289
1355
|
try {
|
|
1290
|
-
const attemptedCredential =
|
|
1356
|
+
const attemptedCredential = requestCredential;
|
|
1291
1357
|
res = await fetchWithReadRetry();
|
|
1292
1358
|
if (res.status === 401 && attemptedCredential.kind === "access_token" && this.refreshCredential) {
|
|
1293
1359
|
await res.body?.cancel().catch(() => void 0);
|
|
1294
1360
|
const refreshed = await this.refreshRejectedAccessToken(attemptedCredential.accessToken);
|
|
1295
1361
|
if (refreshed) {
|
|
1296
|
-
|
|
1362
|
+
requestCredential = refreshed;
|
|
1297
1363
|
res = await fetchWithReadRetry();
|
|
1298
1364
|
}
|
|
1299
1365
|
}
|
|
@@ -1383,18 +1449,23 @@ var TopoloClient = class _TopoloClient {
|
|
|
1383
1449
|
});
|
|
1384
1450
|
return parsed;
|
|
1385
1451
|
}
|
|
1386
|
-
async loadCurrentCredential() {
|
|
1387
|
-
if (!this.credentialProvider) return;
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1452
|
+
async loadCurrentCredential(context) {
|
|
1453
|
+
if (!this.credentialProvider) return this.credential;
|
|
1454
|
+
const key = JSON.stringify(context);
|
|
1455
|
+
let load = this.credentialLoadsInFlight.get(key);
|
|
1456
|
+
if (!load) {
|
|
1457
|
+
load = this.credentialProvider(context).then((credential) => {
|
|
1458
|
+
if (!credential) {
|
|
1459
|
+
throw new TopoloAuthError("The configured Topolo credential is no longer available.");
|
|
1460
|
+
}
|
|
1461
|
+
this.credential = credential;
|
|
1462
|
+
return credential;
|
|
1463
|
+
}).finally(() => {
|
|
1464
|
+
this.credentialLoadsInFlight.delete(key);
|
|
1465
|
+
});
|
|
1466
|
+
this.credentialLoadsInFlight.set(key, load);
|
|
1467
|
+
}
|
|
1468
|
+
return load;
|
|
1398
1469
|
}
|
|
1399
1470
|
async refreshRejectedAccessToken(attemptedAccessToken) {
|
|
1400
1471
|
if (this.credential.kind === "access_token" && this.credential.accessToken !== attemptedAccessToken) {
|
|
@@ -1724,6 +1795,14 @@ var TopoloClient = class _TopoloClient {
|
|
|
1724
1795
|
const entry = await this.getAction(action);
|
|
1725
1796
|
return this.executeResolvedAction(entry, input, options);
|
|
1726
1797
|
}
|
|
1798
|
+
async issueExternalAgentCredential(input) {
|
|
1799
|
+
const actionId = "app_topolo_auth.principal_credentials.issue";
|
|
1800
|
+
const entry = await this.getAction(actionId);
|
|
1801
|
+
if (entry.actionId !== actionId) {
|
|
1802
|
+
throw new TopoloSdkError("unexpected_action", "External agent credential issuance resolved an unexpected action.");
|
|
1803
|
+
}
|
|
1804
|
+
return this.executeResolvedAction(entry, input, { confirm: true }, true);
|
|
1805
|
+
}
|
|
1727
1806
|
async uploadAction(action, content, input, options) {
|
|
1728
1807
|
const entry = await this.getAction(action);
|
|
1729
1808
|
const contract = parseTopoloActionUploadContract(entry.agentContract?.upload);
|
|
@@ -1855,7 +1934,7 @@ var TopoloClient = class _TopoloClient {
|
|
|
1855
1934
|
}
|
|
1856
1935
|
return this.executeResolvedAction(entry, input, options);
|
|
1857
1936
|
}
|
|
1858
|
-
async executeResolvedAction(entry, input, options) {
|
|
1937
|
+
async executeResolvedAction(entry, input, options, preserveSensitiveResult = false) {
|
|
1859
1938
|
const agentAccess = entry.agentAccess ?? (entry.destructive ? "off" : entry.readOnly && !entry.requiresConfirmation ? "auto" : "confirm");
|
|
1860
1939
|
if (agentAccess === "off") {
|
|
1861
1940
|
throw new TopoloPermissionError(
|
|
@@ -1913,7 +1992,7 @@ var TopoloClient = class _TopoloClient {
|
|
|
1913
1992
|
},
|
|
1914
1993
|
confirm: options.confirm === true || entry.readOnly && !entry.destructive && !entry.requiresConfirmation
|
|
1915
1994
|
});
|
|
1916
|
-
return redactActionResult(result);
|
|
1995
|
+
return preserveSensitiveResult ? result : redactActionResult(result);
|
|
1917
1996
|
}
|
|
1918
1997
|
async buildVerifiedAgentActorEnvelope(actionId, input, resource) {
|
|
1919
1998
|
if (!this.actorContext) return void 0;
|
|
@@ -2389,6 +2468,12 @@ function pickString(value) {
|
|
|
2389
2468
|
}
|
|
2390
2469
|
return void 0;
|
|
2391
2470
|
}
|
|
2471
|
+
function requestActionId(headers) {
|
|
2472
|
+
const value = Object.entries(headers ?? {}).find(
|
|
2473
|
+
([name]) => name.toLowerCase() === TOPOLO_ACTION_ID_HEADER.toLowerCase()
|
|
2474
|
+
)?.[1];
|
|
2475
|
+
return value?.trim() || void 0;
|
|
2476
|
+
}
|
|
2392
2477
|
function extractRequired(body) {
|
|
2393
2478
|
if (body && typeof body === "object") {
|
|
2394
2479
|
const maybe = body;
|
|
@@ -3169,10 +3254,15 @@ function createTopolo(options) {
|
|
|
3169
3254
|
};
|
|
3170
3255
|
}
|
|
3171
3256
|
|
|
3172
|
-
// node_modules/.pnpm/@topolo+sdk@0.
|
|
3173
|
-
import { mkdir, readFile, writeFile, chmod, rm, stat } from "fs/promises";
|
|
3257
|
+
// node_modules/.pnpm/@topolo+sdk@0.21.1/node_modules/@topolo/sdk/dist/node/credential-store.js
|
|
3258
|
+
import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2, chmod as chmod2, rm as rm2, stat } from "fs/promises";
|
|
3259
|
+
import { existsSync as existsSync2 } from "fs";
|
|
3260
|
+
import { homedir as homedir2, platform } from "os";
|
|
3261
|
+
import { dirname as dirname2, join as join2 } from "path";
|
|
3262
|
+
import { spawnSync } from "child_process";
|
|
3263
|
+
import { mkdir, readFile, readdir, rename, rm, writeFile, chmod } from "fs/promises";
|
|
3174
3264
|
import { existsSync } from "fs";
|
|
3175
|
-
import { homedir, platform } from "os";
|
|
3265
|
+
import { homedir, platform as hostPlatform } from "os";
|
|
3176
3266
|
import { dirname, join } from "path";
|
|
3177
3267
|
var TopoloSdkError2 = class extends Error {
|
|
3178
3268
|
code;
|
|
@@ -3580,18 +3670,166 @@ function topoloServiceOrigins2(serviceUrls, environment) {
|
|
|
3580
3670
|
])
|
|
3581
3671
|
);
|
|
3582
3672
|
}
|
|
3673
|
+
var KEYCHAIN_SERVICE = "topolo-external-agent-installation";
|
|
3674
|
+
var MACOS_KEYCHAIN_WRITE_SCRIPT = [
|
|
3675
|
+
"log_user 0",
|
|
3676
|
+
"set timeout 10",
|
|
3677
|
+
"set secret [gets stdin]",
|
|
3678
|
+
"spawn -noecho /usr/bin/security add-generic-password -U -a $env(TOPOLO_KEYCHAIN_ACCOUNT) -s $env(TOPOLO_KEYCHAIN_SERVICE) -w",
|
|
3679
|
+
"for {set prompt 0} {$prompt < 2} {incr prompt} {",
|
|
3680
|
+
" expect {",
|
|
3681
|
+
' -re {(?i)password} { send -- "$secret\\r" }',
|
|
3682
|
+
" timeout { exit 70 }",
|
|
3683
|
+
" eof { exit 71 }",
|
|
3684
|
+
" }",
|
|
3685
|
+
"}",
|
|
3686
|
+
"expect eof",
|
|
3687
|
+
"set result [wait]",
|
|
3688
|
+
"exit [lindex $result 3]"
|
|
3689
|
+
].join("\n");
|
|
3690
|
+
function installationStorePath() {
|
|
3691
|
+
const override = process.env.TOPOLO_INSTALLATIONS_PATH?.trim();
|
|
3692
|
+
if (override) return override;
|
|
3693
|
+
const base = process.env.XDG_CONFIG_HOME || (hostPlatform() === "win32" ? join(homedir(), "AppData", "Roaming") : join(homedir(), ".config"));
|
|
3694
|
+
return join(base, "topolo", "installations");
|
|
3695
|
+
}
|
|
3696
|
+
function installationPath(environment, id) {
|
|
3697
|
+
if (!/^[A-Za-z0-9_-]+$/.test(id)) throw new TypeError("Installation id contains unsupported characters.");
|
|
3698
|
+
return join(installationStorePath(), environment, `${id}.json`);
|
|
3699
|
+
}
|
|
3700
|
+
function accountName(environment, id) {
|
|
3701
|
+
return `${environment}:${id}`;
|
|
3702
|
+
}
|
|
3703
|
+
function runOrThrow(result, operation) {
|
|
3704
|
+
if (result.status !== 0) {
|
|
3705
|
+
throw new Error(`${operation} failed: ${(result.stderr || result.error?.message || "platform credential store unavailable").trim()}`);
|
|
3706
|
+
}
|
|
3707
|
+
return result.stdout.trim();
|
|
3708
|
+
}
|
|
3709
|
+
function createPlatformInstallationSecretBackend(options = {}) {
|
|
3710
|
+
const platform2 = options.platform ?? hostPlatform();
|
|
3711
|
+
const spawn = options.spawn ?? ((command, args, spawnOptions) => spawnSync(command, args, spawnOptions));
|
|
3712
|
+
const root = options.root ?? installationStorePath();
|
|
3713
|
+
if (platform2 === "darwin") {
|
|
3714
|
+
return {
|
|
3715
|
+
async set(account, value) {
|
|
3716
|
+
runOrThrow(spawn("/usr/bin/expect", ["-c", MACOS_KEYCHAIN_WRITE_SCRIPT], {
|
|
3717
|
+
input: `${value}
|
|
3718
|
+
`,
|
|
3719
|
+
encoding: "utf8",
|
|
3720
|
+
env: {
|
|
3721
|
+
...process.env,
|
|
3722
|
+
TOPOLO_KEYCHAIN_ACCOUNT: account,
|
|
3723
|
+
TOPOLO_KEYCHAIN_SERVICE: KEYCHAIN_SERVICE
|
|
3724
|
+
}
|
|
3725
|
+
}), "macOS Keychain write");
|
|
3726
|
+
},
|
|
3727
|
+
async get(account) {
|
|
3728
|
+
const result = spawn("security", [
|
|
3729
|
+
"find-generic-password",
|
|
3730
|
+
"-a",
|
|
3731
|
+
account,
|
|
3732
|
+
"-s",
|
|
3733
|
+
KEYCHAIN_SERVICE,
|
|
3734
|
+
"-w"
|
|
3735
|
+
], { encoding: "utf8" });
|
|
3736
|
+
if (result.status === 44) return null;
|
|
3737
|
+
return runOrThrow(result, "macOS Keychain read");
|
|
3738
|
+
},
|
|
3739
|
+
async delete(account) {
|
|
3740
|
+
const result = spawn("security", [
|
|
3741
|
+
"delete-generic-password",
|
|
3742
|
+
"-a",
|
|
3743
|
+
account,
|
|
3744
|
+
"-s",
|
|
3745
|
+
KEYCHAIN_SERVICE
|
|
3746
|
+
], { encoding: "utf8" });
|
|
3747
|
+
if (result.status !== 44) runOrThrow(result, "macOS Keychain delete");
|
|
3748
|
+
}
|
|
3749
|
+
};
|
|
3750
|
+
}
|
|
3751
|
+
if (platform2 === "linux") {
|
|
3752
|
+
return {
|
|
3753
|
+
async set(account, value) {
|
|
3754
|
+
runOrThrow(spawn("secret-tool", [
|
|
3755
|
+
"store",
|
|
3756
|
+
"--label=Topolo external agent installation",
|
|
3757
|
+
"service",
|
|
3758
|
+
KEYCHAIN_SERVICE,
|
|
3759
|
+
"account",
|
|
3760
|
+
account
|
|
3761
|
+
], { input: value, encoding: "utf8" }), "Linux Secret Service write");
|
|
3762
|
+
},
|
|
3763
|
+
async get(account) {
|
|
3764
|
+
const result = spawn("secret-tool", [
|
|
3765
|
+
"lookup",
|
|
3766
|
+
"service",
|
|
3767
|
+
KEYCHAIN_SERVICE,
|
|
3768
|
+
"account",
|
|
3769
|
+
account
|
|
3770
|
+
], { encoding: "utf8" });
|
|
3771
|
+
if (result.status === 1) return null;
|
|
3772
|
+
return runOrThrow(result, "Linux Secret Service read");
|
|
3773
|
+
},
|
|
3774
|
+
async delete(account) {
|
|
3775
|
+
const result = spawn("secret-tool", [
|
|
3776
|
+
"clear",
|
|
3777
|
+
"service",
|
|
3778
|
+
KEYCHAIN_SERVICE,
|
|
3779
|
+
"account",
|
|
3780
|
+
account
|
|
3781
|
+
], { encoding: "utf8" });
|
|
3782
|
+
if (result.status !== 1) runOrThrow(result, "Linux Secret Service delete");
|
|
3783
|
+
}
|
|
3784
|
+
};
|
|
3785
|
+
}
|
|
3786
|
+
if (platform2 === "win32") {
|
|
3787
|
+
const secretPath = (account) => join(root, ".secrets", `${account.replace(":", "-")}.bin`);
|
|
3788
|
+
const powershell = (script, path, input) => spawn(
|
|
3789
|
+
"powershell.exe",
|
|
3790
|
+
["-NoProfile", "-NonInteractive", "-Command", script],
|
|
3791
|
+
{ input, encoding: "utf8", env: { ...process.env, TOPOLO_SECRET_PATH: path } }
|
|
3792
|
+
);
|
|
3793
|
+
return {
|
|
3794
|
+
async set(account, value) {
|
|
3795
|
+
const path = secretPath(account);
|
|
3796
|
+
await mkdir(dirname(path), { recursive: true });
|
|
3797
|
+
const script = "$p=$env:TOPOLO_SECRET_PATH;$v=[Console]::In.ReadToEnd();$b=[Text.Encoding]::UTF8.GetBytes($v);$e=[Security.Cryptography.ProtectedData]::Protect($b,$null,[Security.Cryptography.DataProtectionScope]::CurrentUser);[IO.File]::WriteAllBytes($p,$e)";
|
|
3798
|
+
runOrThrow(powershell(script, path, value), "Windows DPAPI write");
|
|
3799
|
+
},
|
|
3800
|
+
async get(account) {
|
|
3801
|
+
const path = secretPath(account);
|
|
3802
|
+
if (!existsSync(path)) return null;
|
|
3803
|
+
const script = "$p=$env:TOPOLO_SECRET_PATH;$b=[IO.File]::ReadAllBytes($p);$d=[Security.Cryptography.ProtectedData]::Unprotect($b,$null,[Security.Cryptography.DataProtectionScope]::CurrentUser);[Console]::Out.Write([Text.Encoding]::UTF8.GetString($d))";
|
|
3804
|
+
return runOrThrow(powershell(script, path), "Windows DPAPI read");
|
|
3805
|
+
},
|
|
3806
|
+
async delete(account) {
|
|
3807
|
+
await rm(secretPath(account), { force: true });
|
|
3808
|
+
}
|
|
3809
|
+
};
|
|
3810
|
+
}
|
|
3811
|
+
throw new Error(`Secure external-agent installation storage is unsupported on ${platform2}.`);
|
|
3812
|
+
}
|
|
3813
|
+
async function readExternalAgentInstallation(environment, id, backend = createPlatformInstallationSecretBackend()) {
|
|
3814
|
+
const path = installationPath(environment, id);
|
|
3815
|
+
if (!existsSync(path)) return null;
|
|
3816
|
+
const installation = JSON.parse(await readFile(path, "utf8"));
|
|
3817
|
+
const encodedSecret = await backend.get(accountName(environment, id));
|
|
3818
|
+
if (!encodedSecret) throw new Error(`Protected secret is missing for installation ${id}.`);
|
|
3819
|
+
return { installation, secret: JSON.parse(encodedSecret) };
|
|
3820
|
+
}
|
|
3583
3821
|
var ENVIRONMENT_DEFAULT_SERVICE_URLS = TOPOLO_ENVIRONMENT_SERVICE_URLS;
|
|
3584
3822
|
function configPath() {
|
|
3585
3823
|
const override = process.env.TOPOLO_CONFIG_PATH;
|
|
3586
3824
|
if (override) return override;
|
|
3587
|
-
const base = process.env.XDG_CONFIG_HOME || (platform() === "win32" ?
|
|
3588
|
-
return
|
|
3825
|
+
const base = process.env.XDG_CONFIG_HOME || (platform() === "win32" ? join2(homedir2(), "AppData", "Roaming") : join2(homedir2(), ".config"));
|
|
3826
|
+
return join2(base, "topolo", "config.json");
|
|
3589
3827
|
}
|
|
3590
3828
|
async function readConfig() {
|
|
3591
3829
|
const path = configPath();
|
|
3592
|
-
if (!
|
|
3830
|
+
if (!existsSync2(path)) return {};
|
|
3593
3831
|
try {
|
|
3594
|
-
const raw = await
|
|
3832
|
+
const raw = await readFile2(path, "utf8");
|
|
3595
3833
|
return JSON.parse(raw);
|
|
3596
3834
|
} catch {
|
|
3597
3835
|
return {};
|
|
@@ -3599,11 +3837,11 @@ async function readConfig() {
|
|
|
3599
3837
|
}
|
|
3600
3838
|
async function writeConfig(config) {
|
|
3601
3839
|
const path = configPath();
|
|
3602
|
-
await
|
|
3603
|
-
await
|
|
3840
|
+
await mkdir2(dirname2(path), { recursive: true });
|
|
3841
|
+
await writeFile2(path, JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
3604
3842
|
if (platform() !== "win32") {
|
|
3605
3843
|
try {
|
|
3606
|
-
await
|
|
3844
|
+
await chmod2(path, 384);
|
|
3607
3845
|
} catch {
|
|
3608
3846
|
}
|
|
3609
3847
|
}
|
|
@@ -3674,18 +3912,18 @@ var CONFIG_LOCK_TIMEOUT_MS = 5e3;
|
|
|
3674
3912
|
var CONFIG_LOCK_STALE_MS = 12e4;
|
|
3675
3913
|
async function withConfigWriteLock(operation) {
|
|
3676
3914
|
const lockPath = `${configPath()}.lock`;
|
|
3677
|
-
await
|
|
3915
|
+
await mkdir2(dirname2(lockPath), { recursive: true });
|
|
3678
3916
|
const startedAt = Date.now();
|
|
3679
3917
|
while (true) {
|
|
3680
3918
|
try {
|
|
3681
|
-
await
|
|
3919
|
+
await mkdir2(lockPath);
|
|
3682
3920
|
break;
|
|
3683
3921
|
} catch (error) {
|
|
3684
3922
|
if (error.code !== "EEXIST") throw error;
|
|
3685
3923
|
try {
|
|
3686
3924
|
const lock = await stat(lockPath);
|
|
3687
3925
|
if (Date.now() - lock.mtimeMs > CONFIG_LOCK_STALE_MS) {
|
|
3688
|
-
await
|
|
3926
|
+
await rm2(lockPath, { recursive: true, force: true });
|
|
3689
3927
|
continue;
|
|
3690
3928
|
}
|
|
3691
3929
|
} catch (inspectionError) {
|
|
@@ -3701,7 +3939,7 @@ async function withConfigWriteLock(operation) {
|
|
|
3701
3939
|
try {
|
|
3702
3940
|
return await operation();
|
|
3703
3941
|
} finally {
|
|
3704
|
-
await
|
|
3942
|
+
await rm2(lockPath, { recursive: true, force: true });
|
|
3705
3943
|
}
|
|
3706
3944
|
}
|
|
3707
3945
|
async function refreshCredentialIfNeeded(environment = selectedEnvironment(), options = {}) {
|
|
@@ -4539,14 +4777,14 @@ function displayAppId(entry) {
|
|
|
4539
4777
|
|
|
4540
4778
|
// src/version.ts
|
|
4541
4779
|
import { readFileSync } from "fs";
|
|
4542
|
-
import { dirname as
|
|
4780
|
+
import { dirname as dirname3, join as join3 } from "path";
|
|
4543
4781
|
import { fileURLToPath } from "url";
|
|
4544
4782
|
function readMcpVersion() {
|
|
4545
4783
|
try {
|
|
4546
|
-
const here =
|
|
4784
|
+
const here = dirname3(fileURLToPath(import.meta.url));
|
|
4547
4785
|
const candidates = [
|
|
4548
|
-
|
|
4549
|
-
|
|
4786
|
+
join3(here, "package.json"),
|
|
4787
|
+
join3(here, "..", "package.json")
|
|
4550
4788
|
];
|
|
4551
4789
|
for (const path of candidates) {
|
|
4552
4790
|
try {
|
|
@@ -4668,6 +4906,9 @@ function toolError(context, error) {
|
|
|
4668
4906
|
};
|
|
4669
4907
|
}
|
|
4670
4908
|
|
|
4909
|
+
// src/context-credential.ts
|
|
4910
|
+
import { randomUUID } from "crypto";
|
|
4911
|
+
|
|
4671
4912
|
// src/auth.ts
|
|
4672
4913
|
function resolveCredentialFromEnv(environment = "production") {
|
|
4673
4914
|
const prefix = environment === "production" ? "TOPOLO_PRODUCTION" : `TOPOLO_${environment.toUpperCase()}`;
|
|
@@ -4678,12 +4919,102 @@ function resolveCredentialFromEnv(environment = "production") {
|
|
|
4678
4919
|
return null;
|
|
4679
4920
|
}
|
|
4680
4921
|
|
|
4922
|
+
// src/installation-credential.ts
|
|
4923
|
+
function audienceFor(context) {
|
|
4924
|
+
const actionAudience = context.actionId?.match(/^(app_[A-Za-z0-9_]+)\./)?.[1];
|
|
4925
|
+
if (actionAudience) return actionAudience;
|
|
4926
|
+
if (context.actionId) {
|
|
4927
|
+
throw new TopoloMcpPublicError("The requested Topolo action has an invalid audience.");
|
|
4928
|
+
}
|
|
4929
|
+
const service = context.service.trim().toLowerCase();
|
|
4930
|
+
if (/^app_[a-z0-9_]+$/.test(service)) return service;
|
|
4931
|
+
if (!/^[a-z0-9_-]+$/.test(service)) {
|
|
4932
|
+
throw new TopoloMcpPublicError("The requested Topolo service cannot be bound to this installation.");
|
|
4933
|
+
}
|
|
4934
|
+
return `app_topolo_${service.replace(/-/g, "_")}`;
|
|
4935
|
+
}
|
|
4936
|
+
function responseData(value) {
|
|
4937
|
+
if (!value || typeof value !== "object") return {};
|
|
4938
|
+
const record2 = value;
|
|
4939
|
+
return record2.data && typeof record2.data === "object" ? record2.data : record2;
|
|
4940
|
+
}
|
|
4941
|
+
async function resolveExternalAgentInstallationCredential(environment, installationId, taskId, options = {}) {
|
|
4942
|
+
const stored = await (options.readInstallation ?? readExternalAgentInstallation)(environment, installationId);
|
|
4943
|
+
if (!stored) throw new TopoloMcpPublicError(`Installation ${installationId} is not registered on this device.`);
|
|
4944
|
+
if (stored.installation.id !== installationId || stored.installation.environment !== environment) {
|
|
4945
|
+
throw new TopoloMcpPublicError(`Installation ${installationId} is not registered for ${environment}.`);
|
|
4946
|
+
}
|
|
4947
|
+
if (!stored.installation.clientId.trim() || !stored.installation.organizationId.trim() || !/^[A-Za-z0-9_-]{43}$/.test(stored.installation.dpopJkt) || !stored.secret.clientSecret.trim() || !stored.secret.privateJwk.d) {
|
|
4948
|
+
throw new TopoloMcpPublicError(`Installation ${installationId} is incomplete or corrupted.`);
|
|
4949
|
+
}
|
|
4950
|
+
const fetchImpl = options.fetch ?? fetch;
|
|
4951
|
+
const now = options.now ?? Date.now;
|
|
4952
|
+
const authBaseUrl = resolveServiceUrl("auth", options.serviceUrls);
|
|
4953
|
+
const tokenUrl = new URL("/api/developer-oauth/token", authBaseUrl).toString();
|
|
4954
|
+
const cache = /* @__PURE__ */ new Map();
|
|
4955
|
+
const credentialProvider = async (context) => {
|
|
4956
|
+
const audience = audienceFor(context);
|
|
4957
|
+
const resource = context.resource ?? {
|
|
4958
|
+
resourceType: "organization",
|
|
4959
|
+
resourceId: stored.installation.organizationId
|
|
4960
|
+
};
|
|
4961
|
+
const boundTaskId = context.taskId?.trim() || taskId;
|
|
4962
|
+
const key = JSON.stringify({ audience, taskId: boundTaskId, resource });
|
|
4963
|
+
const cached = cache.get(key);
|
|
4964
|
+
if (cached && cached.expiresAt - 15e3 > now()) return cached.credential;
|
|
4965
|
+
const body = new URLSearchParams({
|
|
4966
|
+
grant_type: "client_credentials",
|
|
4967
|
+
client_id: stored.installation.clientId,
|
|
4968
|
+
client_secret: stored.secret.clientSecret,
|
|
4969
|
+
audience,
|
|
4970
|
+
task_id: boundTaskId,
|
|
4971
|
+
resource_type: resource.resourceType,
|
|
4972
|
+
resource_id: resource.resourceId,
|
|
4973
|
+
dpop_jkt: stored.installation.dpopJkt
|
|
4974
|
+
});
|
|
4975
|
+
const response = await fetchImpl(tokenUrl, {
|
|
4976
|
+
method: "POST",
|
|
4977
|
+
headers: {
|
|
4978
|
+
Accept: "application/json",
|
|
4979
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
4980
|
+
},
|
|
4981
|
+
body: body.toString()
|
|
4982
|
+
});
|
|
4983
|
+
const payload = await response.json().catch(() => null);
|
|
4984
|
+
const data = responseData(payload);
|
|
4985
|
+
const accessToken = typeof data.access_token === "string" ? data.access_token : "";
|
|
4986
|
+
const expiresIn = Number(data.expires_in);
|
|
4987
|
+
if (!response.ok || !accessToken || !Number.isFinite(expiresIn) || expiresIn <= 0) {
|
|
4988
|
+
throw new TopoloMcpPublicError("The external-agent installation credential was rejected.");
|
|
4989
|
+
}
|
|
4990
|
+
const credential2 = {
|
|
4991
|
+
kind: "dpop_access_token",
|
|
4992
|
+
accessToken,
|
|
4993
|
+
privateJwk: stored.secret.privateJwk
|
|
4994
|
+
};
|
|
4995
|
+
cache.set(key, { credential: credential2, expiresAt: now() + expiresIn * 1e3 });
|
|
4996
|
+
return credential2;
|
|
4997
|
+
};
|
|
4998
|
+
const credential = await credentialProvider({
|
|
4999
|
+
service: "auth",
|
|
5000
|
+
method: "GET",
|
|
5001
|
+
path: "/api/auth/me",
|
|
5002
|
+
taskId,
|
|
5003
|
+
resource: {
|
|
5004
|
+
resourceType: "organization",
|
|
5005
|
+
resourceId: stored.installation.organizationId
|
|
5006
|
+
}
|
|
5007
|
+
});
|
|
5008
|
+
return { credential, credentialProvider, installation: stored.installation, taskId };
|
|
5009
|
+
}
|
|
5010
|
+
|
|
4681
5011
|
// src/context-credential.ts
|
|
4682
5012
|
var DEFAULT_DEPENDENCIES = {
|
|
4683
5013
|
readConfig,
|
|
4684
5014
|
refreshContextCredentialIfNeeded,
|
|
4685
5015
|
refreshCredentialIfNeeded,
|
|
4686
|
-
resolveCredentialFromEnv
|
|
5016
|
+
resolveCredentialFromEnv,
|
|
5017
|
+
resolveInstallationCredential: resolveExternalAgentInstallationCredential
|
|
4687
5018
|
};
|
|
4688
5019
|
function resolvePinnedContextKey(profile, selector) {
|
|
4689
5020
|
const normalized = selector.trim().toLowerCase();
|
|
@@ -4704,9 +5035,35 @@ function resolvePinnedContextKey(profile, selector) {
|
|
|
4704
5035
|
}
|
|
4705
5036
|
throw new TopoloMcpPublicError(`TOPOLO_CONTEXT "${selector}" is not signed in for this environment`);
|
|
4706
5037
|
}
|
|
4707
|
-
async function resolveMcpCredential(environment, selector = process.env.TOPOLO_CONTEXT, dependencies = DEFAULT_DEPENDENCIES) {
|
|
5038
|
+
async function resolveMcpCredential(environment, selector = process.env.TOPOLO_CONTEXT, dependencies = DEFAULT_DEPENDENCIES, installationSelector = process.env.TOPOLO_AGENT_INSTALLATION_ID, taskSelector = process.env.TOPOLO_AGENT_TASK_ID) {
|
|
4708
5039
|
const environmentCredential = dependencies.resolveCredentialFromEnv(environment);
|
|
4709
5040
|
const requestedSelector = selector?.trim();
|
|
5041
|
+
const installationId = installationSelector?.trim();
|
|
5042
|
+
if (installationId) {
|
|
5043
|
+
if (requestedSelector || environmentCredential) {
|
|
5044
|
+
throw new TopoloMcpPublicError(
|
|
5045
|
+
"TOPOLO_AGENT_INSTALLATION_ID cannot be combined with TOPOLO_CONTEXT or an environment credential"
|
|
5046
|
+
);
|
|
5047
|
+
}
|
|
5048
|
+
const taskId = taskSelector?.trim() || `task_${randomUUID()}`;
|
|
5049
|
+
const config2 = await dependencies.readConfig();
|
|
5050
|
+
const serviceUrls = resolveServiceUrls(
|
|
5051
|
+
profileForEnvironment(config2, environment).serviceUrls,
|
|
5052
|
+
environment
|
|
5053
|
+
);
|
|
5054
|
+
const resolved = await dependencies.resolveInstallationCredential(
|
|
5055
|
+
environment,
|
|
5056
|
+
installationId,
|
|
5057
|
+
taskId,
|
|
5058
|
+
{ serviceUrls }
|
|
5059
|
+
);
|
|
5060
|
+
return {
|
|
5061
|
+
credential: resolved.credential,
|
|
5062
|
+
contextKey: null,
|
|
5063
|
+
credentialProvider: resolved.credentialProvider,
|
|
5064
|
+
taskId: resolved.taskId
|
|
5065
|
+
};
|
|
5066
|
+
}
|
|
4710
5067
|
if (environmentCredential) {
|
|
4711
5068
|
if (requestedSelector) {
|
|
4712
5069
|
throw new TopoloMcpPublicError("TOPOLO_CONTEXT cannot be combined with an environment credential");
|
|
@@ -4809,7 +5166,10 @@ async function runStdioMcp(dependencies = DEFAULT_DEPENDENCIES2) {
|
|
|
4809
5166
|
const topolo = dependencies.createTopolo({
|
|
4810
5167
|
credential,
|
|
4811
5168
|
environment,
|
|
4812
|
-
actorContext: {
|
|
5169
|
+
actorContext: {
|
|
5170
|
+
channel: "mcp",
|
|
5171
|
+
...resolvedCredential.taskId ? { taskId: resolvedCredential.taskId } : {}
|
|
5172
|
+
},
|
|
4813
5173
|
agent: {
|
|
4814
5174
|
clientName: "topolo-mcp",
|
|
4815
5175
|
clientVersion: MCP_VERSION,
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@topolo/mcp",
|
|
3
3
|
"packageManager": "pnpm@10.17.1",
|
|
4
|
-
"version": "0.
|
|
5
|
-
"topoloPublishedVersion": "0.
|
|
4
|
+
"version": "0.13.1",
|
|
5
|
+
"topoloPublishedVersion": "0.13.1",
|
|
6
6
|
"description": "Model Context Protocol server for the Topolo platform. Exposes scope-gated tools that third-party agents (Claude, Codex, etc.) can call natively.",
|
|
7
7
|
"license": "UNLICENSED",
|
|
8
8
|
"author": "Topolo",
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
39
|
-
"@topolo/sdk": "0.
|
|
39
|
+
"@topolo/sdk": "0.21.1"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"@types/node": "^20.12.0",
|