@topolo/mcp 0.12.0 → 0.13.0

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.
Files changed (3) hide show
  1. package/README.md +33 -15
  2. package/dist/index.js +387 -43
  3. 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
- ## Get a credential
27
+ ## Register an accountable agent installation
28
28
 
29
- Either:
29
+ For Codex, Claude, and other persistent agent hosts, register the host once
30
+ from an organization context:
30
31
 
31
- - **Long-lived**: mint an API key at the Topolo Developers console
32
- (`TOPOLO_API_KEY=topo_live_...`). Preferred for persistent agent installs.
33
- - **Short-lived**: run `topolo auth login`. The stdio MCP reads and refreshes
34
- the same environment-specific credential store, so no token copying is
35
- needed. Explicit `TOPOLO_API_KEY` or `TOPOLO_ACCESS_TOKEN` values still take
36
- precedence for headless installations.
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
- Exactly one credential var must be set. If both are present, `TOPOLO_API_KEY`
57
- wins.
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 an injected credential, a pinned stored context, or the environment's
87
- active stored credential. **Refuse to start** if none is available.
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 or the shared CLI
104
- credential store, and every request is authorized by the target Topolo service.
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.17.1/node_modules/@topolo/sdk/dist/index.js
6
+ // node_modules/.pnpm/@topolo+sdk@0.21.0/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", `Bearer ${credential.accessToken}`);
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
- credentialLoadInFlight = null;
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
- await this.loadCurrentCredential();
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: new Headers(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 = this.credential;
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
- applyAuthHeaders(headers, refreshed);
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
- this.credentialLoadInFlight ??= this.credentialProvider().then((credential) => {
1389
- if (!credential) {
1390
- throw new TopoloAuthError("The configured Topolo credential is no longer available.");
1391
- }
1392
- this.credential = credential;
1393
- return credential;
1394
- }).finally(() => {
1395
- this.credentialLoadInFlight = null;
1396
- });
1397
- await this.credentialLoadInFlight;
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.17.1/node_modules/@topolo/sdk/dist/node/credential-store.js
3173
- import { mkdir, readFile, writeFile, chmod, rm, stat } from "fs/promises";
3257
+ // node_modules/.pnpm/@topolo+sdk@0.21.0/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,150 @@ function topoloServiceOrigins2(serviceUrls, environment) {
3580
3670
  ])
3581
3671
  );
3582
3672
  }
3673
+ var KEYCHAIN_SERVICE = "topolo-external-agent-installation";
3674
+ function installationStorePath() {
3675
+ const override = process.env.TOPOLO_INSTALLATIONS_PATH?.trim();
3676
+ if (override) return override;
3677
+ const base = process.env.XDG_CONFIG_HOME || (hostPlatform() === "win32" ? join(homedir(), "AppData", "Roaming") : join(homedir(), ".config"));
3678
+ return join(base, "topolo", "installations");
3679
+ }
3680
+ function installationPath(environment, id) {
3681
+ if (!/^[A-Za-z0-9_-]+$/.test(id)) throw new TypeError("Installation id contains unsupported characters.");
3682
+ return join(installationStorePath(), environment, `${id}.json`);
3683
+ }
3684
+ function accountName(environment, id) {
3685
+ return `${environment}:${id}`;
3686
+ }
3687
+ function runOrThrow(result, operation) {
3688
+ if (result.status !== 0) {
3689
+ throw new Error(`${operation} failed: ${(result.stderr || result.error?.message || "platform credential store unavailable").trim()}`);
3690
+ }
3691
+ return result.stdout.trim();
3692
+ }
3693
+ function createPlatformInstallationSecretBackend(options = {}) {
3694
+ const platform2 = options.platform ?? hostPlatform();
3695
+ const spawn = options.spawn ?? ((command, args, spawnOptions) => spawnSync(command, args, spawnOptions));
3696
+ const root = options.root ?? installationStorePath();
3697
+ if (platform2 === "darwin") {
3698
+ return {
3699
+ async set(account, value) {
3700
+ runOrThrow(spawn("security", [
3701
+ "add-generic-password",
3702
+ "-U",
3703
+ "-a",
3704
+ account,
3705
+ "-s",
3706
+ KEYCHAIN_SERVICE,
3707
+ "-w"
3708
+ ], { input: `${value}
3709
+ `, encoding: "utf8" }), "macOS Keychain write");
3710
+ },
3711
+ async get(account) {
3712
+ const result = spawn("security", [
3713
+ "find-generic-password",
3714
+ "-a",
3715
+ account,
3716
+ "-s",
3717
+ KEYCHAIN_SERVICE,
3718
+ "-w"
3719
+ ], { encoding: "utf8" });
3720
+ if (result.status === 44) return null;
3721
+ return runOrThrow(result, "macOS Keychain read");
3722
+ },
3723
+ async delete(account) {
3724
+ const result = spawn("security", [
3725
+ "delete-generic-password",
3726
+ "-a",
3727
+ account,
3728
+ "-s",
3729
+ KEYCHAIN_SERVICE
3730
+ ], { encoding: "utf8" });
3731
+ if (result.status !== 44) runOrThrow(result, "macOS Keychain delete");
3732
+ }
3733
+ };
3734
+ }
3735
+ if (platform2 === "linux") {
3736
+ return {
3737
+ async set(account, value) {
3738
+ runOrThrow(spawn("secret-tool", [
3739
+ "store",
3740
+ "--label=Topolo external agent installation",
3741
+ "service",
3742
+ KEYCHAIN_SERVICE,
3743
+ "account",
3744
+ account
3745
+ ], { input: value, encoding: "utf8" }), "Linux Secret Service write");
3746
+ },
3747
+ async get(account) {
3748
+ const result = spawn("secret-tool", [
3749
+ "lookup",
3750
+ "service",
3751
+ KEYCHAIN_SERVICE,
3752
+ "account",
3753
+ account
3754
+ ], { encoding: "utf8" });
3755
+ if (result.status === 1) return null;
3756
+ return runOrThrow(result, "Linux Secret Service read");
3757
+ },
3758
+ async delete(account) {
3759
+ const result = spawn("secret-tool", [
3760
+ "clear",
3761
+ "service",
3762
+ KEYCHAIN_SERVICE,
3763
+ "account",
3764
+ account
3765
+ ], { encoding: "utf8" });
3766
+ if (result.status !== 1) runOrThrow(result, "Linux Secret Service delete");
3767
+ }
3768
+ };
3769
+ }
3770
+ if (platform2 === "win32") {
3771
+ const secretPath = (account) => join(root, ".secrets", `${account.replace(":", "-")}.bin`);
3772
+ const powershell = (script, path, input) => spawn(
3773
+ "powershell.exe",
3774
+ ["-NoProfile", "-NonInteractive", "-Command", script],
3775
+ { input, encoding: "utf8", env: { ...process.env, TOPOLO_SECRET_PATH: path } }
3776
+ );
3777
+ return {
3778
+ async set(account, value) {
3779
+ const path = secretPath(account);
3780
+ await mkdir(dirname(path), { recursive: true });
3781
+ 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)";
3782
+ runOrThrow(powershell(script, path, value), "Windows DPAPI write");
3783
+ },
3784
+ async get(account) {
3785
+ const path = secretPath(account);
3786
+ if (!existsSync(path)) return null;
3787
+ 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))";
3788
+ return runOrThrow(powershell(script, path), "Windows DPAPI read");
3789
+ },
3790
+ async delete(account) {
3791
+ await rm(secretPath(account), { force: true });
3792
+ }
3793
+ };
3794
+ }
3795
+ throw new Error(`Secure external-agent installation storage is unsupported on ${platform2}.`);
3796
+ }
3797
+ async function readExternalAgentInstallation(environment, id, backend = createPlatformInstallationSecretBackend()) {
3798
+ const path = installationPath(environment, id);
3799
+ if (!existsSync(path)) return null;
3800
+ const installation = JSON.parse(await readFile(path, "utf8"));
3801
+ const encodedSecret = await backend.get(accountName(environment, id));
3802
+ if (!encodedSecret) throw new Error(`Protected secret is missing for installation ${id}.`);
3803
+ return { installation, secret: JSON.parse(encodedSecret) };
3804
+ }
3583
3805
  var ENVIRONMENT_DEFAULT_SERVICE_URLS = TOPOLO_ENVIRONMENT_SERVICE_URLS;
3584
3806
  function configPath() {
3585
3807
  const override = process.env.TOPOLO_CONFIG_PATH;
3586
3808
  if (override) return override;
3587
- const base = process.env.XDG_CONFIG_HOME || (platform() === "win32" ? join(homedir(), "AppData", "Roaming") : join(homedir(), ".config"));
3588
- return join(base, "topolo", "config.json");
3809
+ const base = process.env.XDG_CONFIG_HOME || (platform() === "win32" ? join2(homedir2(), "AppData", "Roaming") : join2(homedir2(), ".config"));
3810
+ return join2(base, "topolo", "config.json");
3589
3811
  }
3590
3812
  async function readConfig() {
3591
3813
  const path = configPath();
3592
- if (!existsSync(path)) return {};
3814
+ if (!existsSync2(path)) return {};
3593
3815
  try {
3594
- const raw = await readFile(path, "utf8");
3816
+ const raw = await readFile2(path, "utf8");
3595
3817
  return JSON.parse(raw);
3596
3818
  } catch {
3597
3819
  return {};
@@ -3599,11 +3821,11 @@ async function readConfig() {
3599
3821
  }
3600
3822
  async function writeConfig(config) {
3601
3823
  const path = configPath();
3602
- await mkdir(dirname(path), { recursive: true });
3603
- await writeFile(path, JSON.stringify(config, null, 2) + "\n", "utf8");
3824
+ await mkdir2(dirname2(path), { recursive: true });
3825
+ await writeFile2(path, JSON.stringify(config, null, 2) + "\n", "utf8");
3604
3826
  if (platform() !== "win32") {
3605
3827
  try {
3606
- await chmod(path, 384);
3828
+ await chmod2(path, 384);
3607
3829
  } catch {
3608
3830
  }
3609
3831
  }
@@ -3674,18 +3896,18 @@ var CONFIG_LOCK_TIMEOUT_MS = 5e3;
3674
3896
  var CONFIG_LOCK_STALE_MS = 12e4;
3675
3897
  async function withConfigWriteLock(operation) {
3676
3898
  const lockPath = `${configPath()}.lock`;
3677
- await mkdir(dirname(lockPath), { recursive: true });
3899
+ await mkdir2(dirname2(lockPath), { recursive: true });
3678
3900
  const startedAt = Date.now();
3679
3901
  while (true) {
3680
3902
  try {
3681
- await mkdir(lockPath);
3903
+ await mkdir2(lockPath);
3682
3904
  break;
3683
3905
  } catch (error) {
3684
3906
  if (error.code !== "EEXIST") throw error;
3685
3907
  try {
3686
3908
  const lock = await stat(lockPath);
3687
3909
  if (Date.now() - lock.mtimeMs > CONFIG_LOCK_STALE_MS) {
3688
- await rm(lockPath, { recursive: true, force: true });
3910
+ await rm2(lockPath, { recursive: true, force: true });
3689
3911
  continue;
3690
3912
  }
3691
3913
  } catch (inspectionError) {
@@ -3701,7 +3923,7 @@ async function withConfigWriteLock(operation) {
3701
3923
  try {
3702
3924
  return await operation();
3703
3925
  } finally {
3704
- await rm(lockPath, { recursive: true, force: true });
3926
+ await rm2(lockPath, { recursive: true, force: true });
3705
3927
  }
3706
3928
  }
3707
3929
  async function refreshCredentialIfNeeded(environment = selectedEnvironment(), options = {}) {
@@ -4539,14 +4761,14 @@ function displayAppId(entry) {
4539
4761
 
4540
4762
  // src/version.ts
4541
4763
  import { readFileSync } from "fs";
4542
- import { dirname as dirname2, join as join2 } from "path";
4764
+ import { dirname as dirname3, join as join3 } from "path";
4543
4765
  import { fileURLToPath } from "url";
4544
4766
  function readMcpVersion() {
4545
4767
  try {
4546
- const here = dirname2(fileURLToPath(import.meta.url));
4768
+ const here = dirname3(fileURLToPath(import.meta.url));
4547
4769
  const candidates = [
4548
- join2(here, "package.json"),
4549
- join2(here, "..", "package.json")
4770
+ join3(here, "package.json"),
4771
+ join3(here, "..", "package.json")
4550
4772
  ];
4551
4773
  for (const path of candidates) {
4552
4774
  try {
@@ -4668,6 +4890,9 @@ function toolError(context, error) {
4668
4890
  };
4669
4891
  }
4670
4892
 
4893
+ // src/context-credential.ts
4894
+ import { randomUUID } from "crypto";
4895
+
4671
4896
  // src/auth.ts
4672
4897
  function resolveCredentialFromEnv(environment = "production") {
4673
4898
  const prefix = environment === "production" ? "TOPOLO_PRODUCTION" : `TOPOLO_${environment.toUpperCase()}`;
@@ -4678,12 +4903,102 @@ function resolveCredentialFromEnv(environment = "production") {
4678
4903
  return null;
4679
4904
  }
4680
4905
 
4906
+ // src/installation-credential.ts
4907
+ function audienceFor(context) {
4908
+ const actionAudience = context.actionId?.match(/^(app_[A-Za-z0-9_]+)\./)?.[1];
4909
+ if (actionAudience) return actionAudience;
4910
+ if (context.actionId) {
4911
+ throw new TopoloMcpPublicError("The requested Topolo action has an invalid audience.");
4912
+ }
4913
+ const service = context.service.trim().toLowerCase();
4914
+ if (/^app_[a-z0-9_]+$/.test(service)) return service;
4915
+ if (!/^[a-z0-9_-]+$/.test(service)) {
4916
+ throw new TopoloMcpPublicError("The requested Topolo service cannot be bound to this installation.");
4917
+ }
4918
+ return `app_topolo_${service.replace(/-/g, "_")}`;
4919
+ }
4920
+ function responseData(value) {
4921
+ if (!value || typeof value !== "object") return {};
4922
+ const record2 = value;
4923
+ return record2.data && typeof record2.data === "object" ? record2.data : record2;
4924
+ }
4925
+ async function resolveExternalAgentInstallationCredential(environment, installationId, taskId, options = {}) {
4926
+ const stored = await (options.readInstallation ?? readExternalAgentInstallation)(environment, installationId);
4927
+ if (!stored) throw new TopoloMcpPublicError(`Installation ${installationId} is not registered on this device.`);
4928
+ if (stored.installation.id !== installationId || stored.installation.environment !== environment) {
4929
+ throw new TopoloMcpPublicError(`Installation ${installationId} is not registered for ${environment}.`);
4930
+ }
4931
+ 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) {
4932
+ throw new TopoloMcpPublicError(`Installation ${installationId} is incomplete or corrupted.`);
4933
+ }
4934
+ const fetchImpl = options.fetch ?? fetch;
4935
+ const now = options.now ?? Date.now;
4936
+ const authBaseUrl = resolveServiceUrl("auth", options.serviceUrls);
4937
+ const tokenUrl = new URL("/api/developer-oauth/token", authBaseUrl).toString();
4938
+ const cache = /* @__PURE__ */ new Map();
4939
+ const credentialProvider = async (context) => {
4940
+ const audience = audienceFor(context);
4941
+ const resource = context.resource ?? {
4942
+ resourceType: "organization",
4943
+ resourceId: stored.installation.organizationId
4944
+ };
4945
+ const boundTaskId = context.taskId?.trim() || taskId;
4946
+ const key = JSON.stringify({ audience, taskId: boundTaskId, resource });
4947
+ const cached = cache.get(key);
4948
+ if (cached && cached.expiresAt - 15e3 > now()) return cached.credential;
4949
+ const body = new URLSearchParams({
4950
+ grant_type: "client_credentials",
4951
+ client_id: stored.installation.clientId,
4952
+ client_secret: stored.secret.clientSecret,
4953
+ audience,
4954
+ task_id: boundTaskId,
4955
+ resource_type: resource.resourceType,
4956
+ resource_id: resource.resourceId,
4957
+ dpop_jkt: stored.installation.dpopJkt
4958
+ });
4959
+ const response = await fetchImpl(tokenUrl, {
4960
+ method: "POST",
4961
+ headers: {
4962
+ Accept: "application/json",
4963
+ "Content-Type": "application/x-www-form-urlencoded"
4964
+ },
4965
+ body: body.toString()
4966
+ });
4967
+ const payload = await response.json().catch(() => null);
4968
+ const data = responseData(payload);
4969
+ const accessToken = typeof data.access_token === "string" ? data.access_token : "";
4970
+ const expiresIn = Number(data.expires_in);
4971
+ if (!response.ok || !accessToken || !Number.isFinite(expiresIn) || expiresIn <= 0) {
4972
+ throw new TopoloMcpPublicError("The external-agent installation credential was rejected.");
4973
+ }
4974
+ const credential2 = {
4975
+ kind: "dpop_access_token",
4976
+ accessToken,
4977
+ privateJwk: stored.secret.privateJwk
4978
+ };
4979
+ cache.set(key, { credential: credential2, expiresAt: now() + expiresIn * 1e3 });
4980
+ return credential2;
4981
+ };
4982
+ const credential = await credentialProvider({
4983
+ service: "auth",
4984
+ method: "GET",
4985
+ path: "/api/auth/me",
4986
+ taskId,
4987
+ resource: {
4988
+ resourceType: "organization",
4989
+ resourceId: stored.installation.organizationId
4990
+ }
4991
+ });
4992
+ return { credential, credentialProvider, installation: stored.installation, taskId };
4993
+ }
4994
+
4681
4995
  // src/context-credential.ts
4682
4996
  var DEFAULT_DEPENDENCIES = {
4683
4997
  readConfig,
4684
4998
  refreshContextCredentialIfNeeded,
4685
4999
  refreshCredentialIfNeeded,
4686
- resolveCredentialFromEnv
5000
+ resolveCredentialFromEnv,
5001
+ resolveInstallationCredential: resolveExternalAgentInstallationCredential
4687
5002
  };
4688
5003
  function resolvePinnedContextKey(profile, selector) {
4689
5004
  const normalized = selector.trim().toLowerCase();
@@ -4704,9 +5019,35 @@ function resolvePinnedContextKey(profile, selector) {
4704
5019
  }
4705
5020
  throw new TopoloMcpPublicError(`TOPOLO_CONTEXT "${selector}" is not signed in for this environment`);
4706
5021
  }
4707
- async function resolveMcpCredential(environment, selector = process.env.TOPOLO_CONTEXT, dependencies = DEFAULT_DEPENDENCIES) {
5022
+ 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
5023
  const environmentCredential = dependencies.resolveCredentialFromEnv(environment);
4709
5024
  const requestedSelector = selector?.trim();
5025
+ const installationId = installationSelector?.trim();
5026
+ if (installationId) {
5027
+ if (requestedSelector || environmentCredential) {
5028
+ throw new TopoloMcpPublicError(
5029
+ "TOPOLO_AGENT_INSTALLATION_ID cannot be combined with TOPOLO_CONTEXT or an environment credential"
5030
+ );
5031
+ }
5032
+ const taskId = taskSelector?.trim() || `task_${randomUUID()}`;
5033
+ const config2 = await dependencies.readConfig();
5034
+ const serviceUrls = resolveServiceUrls(
5035
+ profileForEnvironment(config2, environment).serviceUrls,
5036
+ environment
5037
+ );
5038
+ const resolved = await dependencies.resolveInstallationCredential(
5039
+ environment,
5040
+ installationId,
5041
+ taskId,
5042
+ { serviceUrls }
5043
+ );
5044
+ return {
5045
+ credential: resolved.credential,
5046
+ contextKey: null,
5047
+ credentialProvider: resolved.credentialProvider,
5048
+ taskId: resolved.taskId
5049
+ };
5050
+ }
4710
5051
  if (environmentCredential) {
4711
5052
  if (requestedSelector) {
4712
5053
  throw new TopoloMcpPublicError("TOPOLO_CONTEXT cannot be combined with an environment credential");
@@ -4809,7 +5150,10 @@ async function runStdioMcp(dependencies = DEFAULT_DEPENDENCIES2) {
4809
5150
  const topolo = dependencies.createTopolo({
4810
5151
  credential,
4811
5152
  environment,
4812
- actorContext: { channel: "mcp" },
5153
+ actorContext: {
5154
+ channel: "mcp",
5155
+ ...resolvedCredential.taskId ? { taskId: resolvedCredential.taskId } : {}
5156
+ },
4813
5157
  agent: {
4814
5158
  clientName: "topolo-mcp",
4815
5159
  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.12.0",
5
- "topoloPublishedVersion": "0.12.0",
4
+ "version": "0.13.0",
5
+ "topoloPublishedVersion": "0.13.0",
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.17.1"
39
+ "@topolo/sdk": "0.21.0"
40
40
  },
41
41
  "devDependencies": {
42
42
  "@types/node": "^20.12.0",