@topolo/cli 0.13.15 → 0.13.16
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 +38 -12
- package/dist/index.js +211 -63
- package/package.json +2 -2
- package/skills/topolo/SKILL.md +12 -2
package/README.md
CHANGED
|
@@ -74,27 +74,50 @@ OAuth errors, and never extends the original device-code expiry.
|
|
|
74
74
|
### Contexts
|
|
75
75
|
|
|
76
76
|
A user can act in their personal space or any organization they belong to. Each
|
|
77
|
-
context has its own credential
|
|
77
|
+
context has its own credential. Name the one to act as with `--context <key>` or
|
|
78
|
+
`TOPOLO_CONTEXT`; without a pin the CLI (and the MCP) use the stored context only
|
|
79
|
+
while it is the only one signed in for the environment.
|
|
78
80
|
These credentials are independent from the shared web preference: switching a
|
|
79
81
|
CLI/MCP context never moves the browser session, and a web switch never changes
|
|
80
82
|
an already-issued CLI/MCP token or its refresh chain.
|
|
81
83
|
|
|
82
84
|
```bash
|
|
83
|
-
topolo context ls # personal + your orgs, and which is
|
|
84
|
-
topolo context use <organization|personal> #
|
|
85
|
+
topolo context ls # personal + your orgs, and which is the stored default
|
|
86
|
+
topolo context use <organization|personal> # set the stored default (validated + a fresh token minted server-side)
|
|
87
|
+
topolo --context org:org_acme whoami # pin one command to one context
|
|
85
88
|
```
|
|
86
89
|
|
|
90
|
+
**No implicit default context.** Once several contexts are signed in for an
|
|
91
|
+
environment, every account command without `--context`/`TOPOLO_CONTEXT` refuses
|
|
92
|
+
with "the context is no longer safe to assume", exactly like the
|
|
93
|
+
multi-environment `--env` guard. `topolo context use` stays available for anyone
|
|
94
|
+
who wants a stored default, but nothing relies on it implicitly.
|
|
95
|
+
|
|
96
|
+
`topolo auth login` stores the approved sign-in under its own context key and
|
|
97
|
+
never moves the stored default (`--use` opts in; the environment's first sign-in
|
|
98
|
+
becomes the default). It prints which user and organization were approved. To
|
|
99
|
+
re-authorise one context, name it:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
topolo --env production --context org:org_acme auth login
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
If the browser approves a different user or organization, the CLI warns that
|
|
106
|
+
`org:org_acme` was not renewed and exits 1.
|
|
107
|
+
|
|
87
108
|
### Wire into agent hosts
|
|
88
109
|
|
|
89
110
|
```bash
|
|
90
|
-
topolo setup #
|
|
111
|
+
topolo setup # registers the exact bundled MCP, pinned to the only signed-in context
|
|
91
112
|
topolo setup --all # configure every supported local host
|
|
92
113
|
topolo doctor # verifies auth, handshake, tool discovery, and identity
|
|
93
114
|
```
|
|
94
115
|
|
|
95
116
|
The MCP reads the same credential `topolo auth login` writes — no API key to paste.
|
|
96
|
-
|
|
97
|
-
|
|
117
|
+
Every registration setup writes pins `TOPOLO_ENV` and `TOPOLO_CONTEXT`. Without
|
|
118
|
+
`--context`, setup pins the only signed-in context, and refuses when there are
|
|
119
|
+
none or several. For each additional organization, use an exact context key
|
|
120
|
+
returned by `topolo context ls --json`:
|
|
98
121
|
|
|
99
122
|
```bash
|
|
100
123
|
topolo setup --name topolo-acme --context org:org_acme --env production
|
|
@@ -133,12 +156,14 @@ topolo mcp serve [--port 7337] # what the keep-alive unit runs; foregrou
|
|
|
133
156
|
`setup --shared`:
|
|
134
157
|
|
|
135
158
|
1. Adds the identity (`{name, env, context}`, exactly what a stdio entry pins
|
|
136
|
-
with `TOPOLO_ENV` + `TOPOLO_CONTEXT`;
|
|
137
|
-
`
|
|
159
|
+
with `TOPOLO_ENV` + `TOPOLO_CONTEXT`; both are always pinned, and without
|
|
160
|
+
`--context` the only signed-in context is pinned) to `mcp-hub.json` next to
|
|
138
161
|
the credential store (`~/.config/topolo/mcp-hub.json`, or the directory of
|
|
139
162
|
`TOPOLO_CONFIG_PATH`). `--import-stdio` converts existing stdio entries and
|
|
140
|
-
skips ones the hub cannot serve (
|
|
141
|
-
agent installations, invalid or
|
|
163
|
+
skips ones the hub cannot serve (entries with no `TOPOLO_CONTEXT` pin,
|
|
164
|
+
private `TOPOLO_CONFIG_PATH` profiles, agent installations, invalid or
|
|
165
|
+
conflicting definitions). Add a skipped unpinned entry back with
|
|
166
|
+
`--name <name> --context <context>`.
|
|
142
167
|
2. Creates `mcp-hub.token` (256-bit, mode 0600) once and reuses it.
|
|
143
168
|
3. Registers **every** hub identity with Claude Code and Codex at user scope,
|
|
144
169
|
replacing same-name stdio entries. `--project` is refused so the token can
|
|
@@ -199,8 +224,9 @@ Hub behaviour and security:
|
|
|
199
224
|
## Cross-org isolation
|
|
200
225
|
|
|
201
226
|
The CLI **never accepts an `orgId` argument**. Every command is scoped to the
|
|
202
|
-
|
|
203
|
-
in a different organization you
|
|
227
|
+
context embedded in the credential (personal or a specific org); to act
|
|
228
|
+
in a different organization you pin that context's own stored credential
|
|
229
|
+
(`--context`), or create one with `topolo context use`, which
|
|
204
230
|
mints a new context-scoped credential — you can never target another org by
|
|
205
231
|
passing an id. The context remains bound through refresh rotation and is not
|
|
206
232
|
re-hydrated from the user's current web selection. This is enforced in two places:
|
package/dist/index.js
CHANGED
|
@@ -5599,6 +5599,70 @@ function resolveOAuthClientId(config, environment = selectedEnvironment()) {
|
|
|
5599
5599
|
if (process.env.TOPOLO_CLI_CLIENT_ID) return process.env.TOPOLO_CLI_CLIENT_ID;
|
|
5600
5600
|
return DEFAULT_CLI_CLIENT_IDS[environment] ?? profileForEnvironment(config, environment).oauthClientId;
|
|
5601
5601
|
}
|
|
5602
|
+
function signedInContextKeys(config, environment) {
|
|
5603
|
+
return Object.keys(profileForEnvironment(config, environment).contexts ?? {});
|
|
5604
|
+
}
|
|
5605
|
+
function matchContextSelector(config, environment, selector) {
|
|
5606
|
+
const normalized = selector.trim().toLowerCase();
|
|
5607
|
+
if (!normalized || /[\r\n]/.test(selector)) {
|
|
5608
|
+
throw new Error("Context must be a non-empty single-line identifier.");
|
|
5609
|
+
}
|
|
5610
|
+
const contexts = Object.entries(profileForEnvironment(config, environment).contexts ?? {});
|
|
5611
|
+
const exact = contexts.find(([key]) => key.toLowerCase() === normalized);
|
|
5612
|
+
if (exact) return exact[0];
|
|
5613
|
+
const matches = contexts.filter(([, context]) => [
|
|
5614
|
+
context.organizationId,
|
|
5615
|
+
context.organizationSlug,
|
|
5616
|
+
context.organizationName
|
|
5617
|
+
].some((value) => String(value || "").trim().toLowerCase() === normalized));
|
|
5618
|
+
if (matches.length > 1) {
|
|
5619
|
+
throw new Error(`Context "${selector.trim()}" is ambiguous in ${environment}; use an exact key from \`topolo --env ${environment} context ls\`.`);
|
|
5620
|
+
}
|
|
5621
|
+
return matches[0]?.[0] ?? null;
|
|
5622
|
+
}
|
|
5623
|
+
function commandContextKey(config, environment, explicit = process.env.TOPOLO_CONTEXT) {
|
|
5624
|
+
const selector = explicit?.trim();
|
|
5625
|
+
if (selector) return matchContextSelector(config, environment, selector) ?? selector;
|
|
5626
|
+
return implicitContextKey(config, environment) ?? void 0;
|
|
5627
|
+
}
|
|
5628
|
+
function implicitContextKey(config, environment) {
|
|
5629
|
+
if (credentialSourceFromConfig(config, environment).startsWith("env:")) return null;
|
|
5630
|
+
const keys = signedInContextKeys(config, environment);
|
|
5631
|
+
if (keys.length > 1) throw new ContextNotSafeToAssumeError(environment, keys);
|
|
5632
|
+
return keys[0] ?? null;
|
|
5633
|
+
}
|
|
5634
|
+
var ContextNotSafeToAssumeError = class extends Error {
|
|
5635
|
+
constructor(environment, contexts) {
|
|
5636
|
+
super(
|
|
5637
|
+
`This machine is signed in to ${contexts.length} Topolo contexts in ${environment} (${contexts.join(", ")}), so the context is no longer safe to assume.
|
|
5638
|
+
|
|
5639
|
+
State it explicitly:
|
|
5640
|
+
topolo --env ${environment} --context <${contexts.join("|")}> ...
|
|
5641
|
+
|
|
5642
|
+
Or set it once for the whole session:
|
|
5643
|
+
export TOPOLO_CONTEXT=<${contexts.join("|")}>`
|
|
5644
|
+
);
|
|
5645
|
+
this.environment = environment;
|
|
5646
|
+
this.contexts = contexts;
|
|
5647
|
+
this.name = "ContextNotSafeToAssumeError";
|
|
5648
|
+
}
|
|
5649
|
+
environment;
|
|
5650
|
+
contexts;
|
|
5651
|
+
};
|
|
5652
|
+
async function withPinnedContext(options, read = () => readConfig()) {
|
|
5653
|
+
if (options.context?.trim() || options.installationId?.trim()) return options;
|
|
5654
|
+
const environment = normalizeEnvironment(options.environment?.trim() || process.env.TOPOLO_ENV || "production");
|
|
5655
|
+
const keys = signedInContextKeys(await read(), environment);
|
|
5656
|
+
if (keys.length === 1) return { ...options, context: keys[0] };
|
|
5657
|
+
if (!keys.length) {
|
|
5658
|
+
throw new Error(
|
|
5659
|
+
`No Topolo context is signed in for ${environment}, so there is nothing to pin. Run \`topolo --env ${environment} auth login\` first, or pass --context <context>.`
|
|
5660
|
+
);
|
|
5661
|
+
}
|
|
5662
|
+
throw new Error(
|
|
5663
|
+
`${keys.length} Topolo contexts are signed in for ${environment} (${keys.join(", ")}). Pass --context <context> to choose the one this MCP registration pins.`
|
|
5664
|
+
);
|
|
5665
|
+
}
|
|
5602
5666
|
|
|
5603
5667
|
// src/output.ts
|
|
5604
5668
|
function resolveOutputMode(override) {
|
|
@@ -5705,7 +5769,14 @@ var CLI_VERSION = readCliVersion();
|
|
|
5705
5769
|
// src/commands/auth.ts
|
|
5706
5770
|
function registerAuthCommands(program2) {
|
|
5707
5771
|
const auth = program2.command("auth").description("Manage Topolo credentials");
|
|
5708
|
-
auth.command("login").description("Authenticate with Topolo via OAuth device flow (default) or store an API key").option("--api-key <key>", "Store an API key directly, skipping the OAuth flow").option("--client-id <id>", "OAuth client_id (or set TOPOLO_CLI_CLIENT_ID)").option("--scope <scope>", "Scopes to request (space-separated)").option("--no-browser", "Print the approval URL without opening a browser").option("--json", "Output JSON").option("--no-json", "Output human-readable text").
|
|
5772
|
+
auth.command("login").description("Authenticate with Topolo via OAuth device flow (default) or store an API key").option("--api-key <key>", "Store an API key directly, skipping the OAuth flow").option("--client-id <id>", "OAuth client_id (or set TOPOLO_CLI_CLIENT_ID)").option("--scope <scope>", "Scopes to request (space-separated)").option("--no-browser", "Print the approval URL without opening a browser").option("--use", "Also make the approved context this environment's stored default (as `topolo context use`)").option("--json", "Output JSON").option("--no-json", "Output human-readable text").addHelpText("after", [
|
|
5773
|
+
"",
|
|
5774
|
+
"The approved sign-in is stored under its own context key (personal or org:<id>).",
|
|
5775
|
+
"It never changes the stored default context unless you pass --use, or it is the",
|
|
5776
|
+
"first context signed in for the environment. To re-authorise one context, name it:",
|
|
5777
|
+
" topolo --env <env> --context <context> auth login",
|
|
5778
|
+
"and the CLI warns (and exits 1) if the browser approves a different user or org."
|
|
5779
|
+
].join("\n")).action(
|
|
5709
5780
|
async (opts) => {
|
|
5710
5781
|
const mode = resolveOutputMode(opts.json);
|
|
5711
5782
|
const environment = selectedEnvironment();
|
|
@@ -5739,7 +5810,9 @@ Visit ${apiKeyIssuanceUrl(environment)} to mint one, or configure an OAuth clien
|
|
|
5739
5810
|
environment,
|
|
5740
5811
|
scope: opts.scope,
|
|
5741
5812
|
mode,
|
|
5742
|
-
openBrowser: opts.browser !== false
|
|
5813
|
+
openBrowser: opts.browser !== false,
|
|
5814
|
+
makeDefault: opts.use === true,
|
|
5815
|
+
requestedContext: process.env.TOPOLO_CONTEXT?.trim() || void 0
|
|
5743
5816
|
});
|
|
5744
5817
|
} catch (err) {
|
|
5745
5818
|
fail2(err, mode);
|
|
@@ -5856,18 +5929,44 @@ async function runDeviceFlow(options) {
|
|
|
5856
5929
|
...token.refresh_token ? { refreshToken: token.refresh_token } : {},
|
|
5857
5930
|
expiresAt: Math.floor(Date.now() / 1e3) + token.expires_in
|
|
5858
5931
|
};
|
|
5859
|
-
const context = await resolveLoginContext(
|
|
5932
|
+
const { context, identity } = await resolveLoginContext(
|
|
5860
5933
|
credential,
|
|
5861
5934
|
resolved.serviceUrls,
|
|
5862
5935
|
options.clientId,
|
|
5863
5936
|
options.environment
|
|
5864
5937
|
);
|
|
5938
|
+
const approvedKey = contextKey(context.contextType, context.organizationId);
|
|
5865
5939
|
const updated = await readConfig();
|
|
5866
|
-
|
|
5940
|
+
const previousDefault = profileDefault(updated, options.environment);
|
|
5941
|
+
const makeDefault = options.makeDefault === true || signedInContextKeys(updated, options.environment).length === 0;
|
|
5942
|
+
await writeConfig(writeContextCredential(updated, options.environment, context, makeDefault));
|
|
5943
|
+
const defaultChanged = makeDefault && previousDefault !== approvedKey;
|
|
5944
|
+
const requested = options.requestedContext ? requestedContextKey(updated, options.environment, options.requestedContext) : void 0;
|
|
5945
|
+
const requestedRenewed = requested === void 0 || identity !== null && loginMatches(requested, context);
|
|
5946
|
+
const approved = {
|
|
5947
|
+
contextKey: approvedKey,
|
|
5948
|
+
user: identity ? { email: identity.user.email, name: identity.user.name } : null,
|
|
5949
|
+
organization: context.contextType === "organization" ? { id: context.organizationId, slug: context.organizationSlug ?? null, name: context.organizationName ?? null } : null
|
|
5950
|
+
};
|
|
5951
|
+
const who = identity ? identity.user.email : "an identity that could not be verified";
|
|
5952
|
+
const where = context.contextType === "organization" ? `${context.organizationName || context.organizationSlug || context.organizationId} (${approvedKey})` : `personal context (${approvedKey})`;
|
|
5953
|
+
if (!requestedRenewed) {
|
|
5954
|
+
process.stderr.write(
|
|
5955
|
+
`
|
|
5956
|
+
\u26A0 You asked to re-authorise "${options.requestedContext}", but the browser approved ${who} in ${where}.
|
|
5957
|
+
"${options.requestedContext}" was NOT renewed. The approval was stored under ${approvedKey} only.
|
|
5958
|
+
Sign the browser in as the user for "${options.requestedContext}" and run this again.
|
|
5959
|
+
`
|
|
5960
|
+
);
|
|
5961
|
+
process.exitCode = 1;
|
|
5962
|
+
}
|
|
5867
5963
|
emit(
|
|
5868
5964
|
{
|
|
5869
|
-
ok:
|
|
5965
|
+
ok: requestedRenewed,
|
|
5870
5966
|
environment: options.environment,
|
|
5967
|
+
approved,
|
|
5968
|
+
...options.requestedContext ? { requestedContext: options.requestedContext, requestedContextRenewed: requestedRenewed } : {},
|
|
5969
|
+
defaultContext: { changed: defaultChanged, key: makeDefault ? approvedKey : previousDefault ?? null },
|
|
5871
5970
|
credential: {
|
|
5872
5971
|
kind: "access_token",
|
|
5873
5972
|
expiresIn: token.expires_in,
|
|
@@ -5875,10 +5974,34 @@ async function runDeviceFlow(options) {
|
|
|
5875
5974
|
}
|
|
5876
5975
|
},
|
|
5877
5976
|
options.mode,
|
|
5878
|
-
() =>
|
|
5977
|
+
() => [
|
|
5978
|
+
"",
|
|
5979
|
+
` \u2713 Signed in to Topolo ${environmentLabel(options.environment)}.`,
|
|
5980
|
+
` Approved as: ${who}${identity?.user.name ? ` (${identity.user.name})` : ""}`,
|
|
5981
|
+
` Context: ${where}`,
|
|
5982
|
+
defaultChanged ? ` Default: ${approvedKey} is now this environment's stored default context.` : ` Default: unchanged${previousDefault ? ` (${previousDefault})` : ""}. Nothing acts as ${approvedKey} unless pinned.`,
|
|
5983
|
+
` Use it with: topolo --env ${options.environment} --context ${approvedKey} <command>`
|
|
5984
|
+
].join("\n")
|
|
5879
5985
|
);
|
|
5880
5986
|
}
|
|
5987
|
+
function profileDefault(config, environment) {
|
|
5988
|
+
return config.profiles?.[environment]?.activeContext;
|
|
5989
|
+
}
|
|
5990
|
+
function requestedContextKey(config, environment, selector) {
|
|
5991
|
+
try {
|
|
5992
|
+
return matchContextSelector(config, environment, selector) ?? selector.trim();
|
|
5993
|
+
} catch {
|
|
5994
|
+
return selector.trim();
|
|
5995
|
+
}
|
|
5996
|
+
}
|
|
5997
|
+
function loginMatches(requested, context) {
|
|
5998
|
+
const wanted = requested.toLowerCase();
|
|
5999
|
+
if (wanted === contextKey(context.contextType, context.organizationId).toLowerCase()) return true;
|
|
6000
|
+
if (context.contextType !== "organization") return false;
|
|
6001
|
+
return [context.organizationId, context.organizationSlug, context.organizationName].some((value) => String(value || "").trim().toLowerCase() === wanted);
|
|
6002
|
+
}
|
|
5881
6003
|
async function resolveLoginContext(credential, serviceUrls, clientId, environment) {
|
|
6004
|
+
let identity = null;
|
|
5882
6005
|
try {
|
|
5883
6006
|
const topolo = createTopolo({
|
|
5884
6007
|
credential,
|
|
@@ -5886,21 +6009,24 @@ async function resolveLoginContext(credential, serviceUrls, clientId, environmen
|
|
|
5886
6009
|
agent: { clientName: "topolo-cli", clientVersion: CLI_VERSION },
|
|
5887
6010
|
serviceUrls
|
|
5888
6011
|
});
|
|
5889
|
-
|
|
5890
|
-
const org =
|
|
6012
|
+
identity = await topolo.identity.whoami();
|
|
6013
|
+
const org = identity.organization;
|
|
5891
6014
|
if (org?.id) {
|
|
5892
6015
|
return {
|
|
5893
|
-
|
|
5894
|
-
|
|
5895
|
-
|
|
5896
|
-
|
|
5897
|
-
|
|
5898
|
-
|
|
6016
|
+
identity,
|
|
6017
|
+
context: {
|
|
6018
|
+
contextType: "organization",
|
|
6019
|
+
organizationId: org.id,
|
|
6020
|
+
organizationSlug: org.slug,
|
|
6021
|
+
organizationName: org.name,
|
|
6022
|
+
credential,
|
|
6023
|
+
oauthClientId: clientId
|
|
6024
|
+
}
|
|
5899
6025
|
};
|
|
5900
6026
|
}
|
|
5901
6027
|
} catch {
|
|
5902
6028
|
}
|
|
5903
|
-
return { contextType: "personal", credential, oauthClientId: clientId };
|
|
6029
|
+
return { identity, context: { contextType: "personal", credential, oauthClientId: clientId } };
|
|
5904
6030
|
}
|
|
5905
6031
|
function tryOpenBrowser(url) {
|
|
5906
6032
|
try {
|
|
@@ -5937,10 +6063,10 @@ function startSpinner() {
|
|
|
5937
6063
|
// src/client.ts
|
|
5938
6064
|
async function buildClient(options) {
|
|
5939
6065
|
const environment = options?.environment ?? selectedEnvironment();
|
|
5940
|
-
const
|
|
6066
|
+
const config = await readConfig();
|
|
6067
|
+
const context = commandContextKey(config, environment, options?.context?.trim() || process.env.TOPOLO_CONTEXT);
|
|
5941
6068
|
const credential = context ? await refreshContextCredentialIfNeeded(environment, context) : await refreshCredentialIfNeeded(environment);
|
|
5942
6069
|
if (!credential) return null;
|
|
5943
|
-
const config = await readConfig();
|
|
5944
6070
|
const profile = resolvedProfileConfig(config, environment);
|
|
5945
6071
|
const topolo = createTopolo({
|
|
5946
6072
|
credential,
|
|
@@ -6384,11 +6510,10 @@ function tryOpenApproval(url) {
|
|
|
6384
6510
|
async function actionExecutionContext(topolo) {
|
|
6385
6511
|
const environment = selectedEnvironment();
|
|
6386
6512
|
const config = await readConfig();
|
|
6387
|
-
const profile = profileForEnvironment(config, environment);
|
|
6388
6513
|
const source = credentialSourceFromConfig(config, environment);
|
|
6389
6514
|
return createTopoloAgentExecutionContext({
|
|
6390
6515
|
environment,
|
|
6391
|
-
contextKey: source.startsWith("env:") ? null :
|
|
6516
|
+
contextKey: source.startsWith("env:") ? null : commandContextKey(config, environment) ?? null,
|
|
6392
6517
|
identity: await topolo.identity.whoami(),
|
|
6393
6518
|
serviceUrls: resolvedProfileConfig(config, environment).serviceUrls
|
|
6394
6519
|
});
|
|
@@ -7129,13 +7254,14 @@ function createOAuthClientPayload(options) {
|
|
|
7129
7254
|
}
|
|
7130
7255
|
async function authFetchJson(path, init = {}) {
|
|
7131
7256
|
const environment = selectedEnvironment();
|
|
7132
|
-
const
|
|
7257
|
+
const config = await readConfig();
|
|
7258
|
+
const context = commandContextKey(config, environment);
|
|
7259
|
+
const credential = context ? await refreshContextCredentialIfNeeded(environment, context) : await refreshCredentialIfNeeded(environment);
|
|
7133
7260
|
if (!credential) {
|
|
7134
7261
|
const err = new Error("Topolo credentials are required.");
|
|
7135
7262
|
err.code = "auth_error";
|
|
7136
7263
|
throw err;
|
|
7137
7264
|
}
|
|
7138
|
-
const config = await readConfig();
|
|
7139
7265
|
const profile = resolvedProfileConfig(config, environment);
|
|
7140
7266
|
const authBaseUrl = (profile.serviceUrls?.auth ?? "https://auth.topolo.app").replace(/\/+$/, "");
|
|
7141
7267
|
const headers = new Headers(init.headers);
|
|
@@ -7356,6 +7482,12 @@ import { dirname as dirname7, join as join11, resolve as resolve3 } from "path";
|
|
|
7356
7482
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
7357
7483
|
import { createRequire } from "module";
|
|
7358
7484
|
|
|
7485
|
+
// src/lib/command-context.ts
|
|
7486
|
+
function withProgramContext(opts, command) {
|
|
7487
|
+
const context = opts.context ?? command.optsWithGlobals().context;
|
|
7488
|
+
return context === void 0 ? opts : { ...opts, context };
|
|
7489
|
+
}
|
|
7490
|
+
|
|
7359
7491
|
// src/commands/agent-host.ts
|
|
7360
7492
|
import { spawnSync as spawnSync3 } from "child_process";
|
|
7361
7493
|
import { existsSync as existsSync4 } from "fs";
|
|
@@ -7939,7 +8071,10 @@ function registrationEnvironment(agent, registration) {
|
|
|
7939
8071
|
TOPOLO_AGENT_NAME: AGENT_NAME_FOR[agent],
|
|
7940
8072
|
...registration.configPath ? { TOPOLO_CONFIG_PATH: registration.configPath } : {},
|
|
7941
8073
|
...registration.installationId ? { TOPOLO_AGENT_INSTALLATION_ID: registration.installationId } : {},
|
|
7942
|
-
|
|
8074
|
+
// Every registration pins its environment, and its context unless it is
|
|
8075
|
+
// bound to an installation, so no MCP ever follows a stored default.
|
|
8076
|
+
TOPOLO_ENV: registration.environment,
|
|
8077
|
+
...registration.context ? { TOPOLO_CONTEXT: registration.context } : {},
|
|
7943
8078
|
...privateAliases
|
|
7944
8079
|
};
|
|
7945
8080
|
}
|
|
@@ -8211,14 +8346,13 @@ function validateMcpHubIdentity(value, where = "identity") {
|
|
|
8211
8346
|
throw new Error(`MCP hub identity ${name} env must be one of ${MCP_HUB_ENVIRONMENTS.join(", ")}.`);
|
|
8212
8347
|
}
|
|
8213
8348
|
const context = record3.context;
|
|
8214
|
-
if (context
|
|
8349
|
+
if (context === void 0) {
|
|
8350
|
+
throw new Error(`MCP hub identity ${name} must pin a context (a key from \`topolo context ls\`).`);
|
|
8351
|
+
}
|
|
8352
|
+
if (typeof context !== "string" || !context.trim() || /[\r\n]/.test(context)) {
|
|
8215
8353
|
throw new Error(`MCP hub identity ${name} context must be a non-empty single-line selector.`);
|
|
8216
8354
|
}
|
|
8217
|
-
return {
|
|
8218
|
-
name,
|
|
8219
|
-
environment,
|
|
8220
|
-
...context !== void 0 ? { context: context.trim() } : {}
|
|
8221
|
-
};
|
|
8355
|
+
return { name, environment, context: context.trim() };
|
|
8222
8356
|
}
|
|
8223
8357
|
function parseMcpHubConfig(text2, source) {
|
|
8224
8358
|
let parsed;
|
|
@@ -8257,7 +8391,7 @@ function serializeMcpHubConfig(config) {
|
|
|
8257
8391
|
identities: config.identities.map((identity) => ({
|
|
8258
8392
|
name: identity.name,
|
|
8259
8393
|
env: identity.environment,
|
|
8260
|
-
|
|
8394
|
+
context: identity.context
|
|
8261
8395
|
}))
|
|
8262
8396
|
}, null, 2) + "\n";
|
|
8263
8397
|
}
|
|
@@ -8500,12 +8634,21 @@ async function importStdioIdentities(agents, homeDir, codexHome) {
|
|
|
8500
8634
|
skipped.push({ agent, name: registration.name, reason: "uses a private config path or agent installation; keep it on stdio" });
|
|
8501
8635
|
continue;
|
|
8502
8636
|
}
|
|
8637
|
+
if (!registration.env.TOPOLO_CONTEXT) {
|
|
8638
|
+
skipped.push({
|
|
8639
|
+
agent,
|
|
8640
|
+
name: registration.name,
|
|
8641
|
+
reason: "is not pinned to a context; add it with `topolo setup --shared --name <name> --context <context>`"
|
|
8642
|
+
});
|
|
8643
|
+
continue;
|
|
8644
|
+
}
|
|
8503
8645
|
let identity;
|
|
8504
8646
|
try {
|
|
8505
8647
|
identity = validateMcpHubIdentity({
|
|
8506
8648
|
name: registration.name,
|
|
8649
|
+
// A stdio entry without TOPOLO_ENV has always meant production.
|
|
8507
8650
|
env: registration.env.TOPOLO_ENV || "production",
|
|
8508
|
-
|
|
8651
|
+
context: registration.env.TOPOLO_CONTEXT
|
|
8509
8652
|
});
|
|
8510
8653
|
} catch (error) {
|
|
8511
8654
|
skipped.push({ agent, name: registration.name, reason: error.message });
|
|
@@ -8547,16 +8690,9 @@ async function installSharedAgentSurfaces(options, dependencies) {
|
|
|
8547
8690
|
}
|
|
8548
8691
|
if (!options.importStdio || options.name) {
|
|
8549
8692
|
const name = options.name?.trim() || "topolo";
|
|
8550
|
-
const
|
|
8551
|
-
|
|
8552
|
-
|
|
8553
|
-
throw new Error("Pinned contexts require --name topolo-<name> so the active topolo registration remains independent.");
|
|
8554
|
-
}
|
|
8555
|
-
config = upsertMcpHubIdentity(config, validateMcpHubIdentity({
|
|
8556
|
-
name,
|
|
8557
|
-
env: options.environment?.trim() || dependencies.environment.TOPOLO_ENV || "production",
|
|
8558
|
-
...context ? { context } : {}
|
|
8559
|
-
}));
|
|
8693
|
+
const env = normalizeEnvironment(options.environment?.trim() || dependencies.environment.TOPOLO_ENV || "production");
|
|
8694
|
+
const { context } = await withPinnedContext({ context: options.context, environment: env }, dependencies.readCredentialStore);
|
|
8695
|
+
config = upsertMcpHubIdentity(config, validateMcpHubIdentity({ name, env, context }));
|
|
8560
8696
|
}
|
|
8561
8697
|
await writeMcpHubConfig(paths.config, config);
|
|
8562
8698
|
const { token, created } = await ensureMcpHubToken(paths.token);
|
|
@@ -8635,7 +8771,8 @@ function defaultSharedSetupDependencies(skillsDir, realHome) {
|
|
|
8635
8771
|
nodeCommand: stableNodeCommand,
|
|
8636
8772
|
cliEntry: () => resolveCliEntry(),
|
|
8637
8773
|
environment: process.env,
|
|
8638
|
-
run: runCommand
|
|
8774
|
+
run: runCommand,
|
|
8775
|
+
readCredentialStore: readConfig
|
|
8639
8776
|
};
|
|
8640
8777
|
}
|
|
8641
8778
|
function formatSharedSetup(result) {
|
|
@@ -8688,11 +8825,10 @@ function resolveMcpRegistration(options) {
|
|
|
8688
8825
|
if (name !== "topolo" && !/^topolo-[a-z0-9][a-z0-9_-]*$/.test(name)) {
|
|
8689
8826
|
throw new Error("Named registrations must use a topolo-<name> identifier.");
|
|
8690
8827
|
}
|
|
8691
|
-
if (context &&
|
|
8692
|
-
throw new Error(
|
|
8693
|
-
|
|
8694
|
-
|
|
8695
|
-
throw new Error("Named registrations require --context <context>.");
|
|
8828
|
+
if (!context && !options.installationId?.trim()) {
|
|
8829
|
+
throw new Error(
|
|
8830
|
+
"MCP registrations must pin a context: pass --context <context> (see `topolo context ls`), or --installation <id>."
|
|
8831
|
+
);
|
|
8696
8832
|
}
|
|
8697
8833
|
if (context && /[\r\n]/.test(context)) throw new Error("Context must be a single-line identifier.");
|
|
8698
8834
|
if (!["production", "staging", "development", "local"].includes(environment)) {
|
|
@@ -8713,24 +8849,25 @@ function resolveMcpRegistration(options) {
|
|
|
8713
8849
|
}
|
|
8714
8850
|
async function installForAgent(agent, sourceDir, mcp, opts) {
|
|
8715
8851
|
const { homeDir, projectDir, codexHome } = opts.target;
|
|
8716
|
-
|
|
8852
|
+
const registration = opts.registration;
|
|
8853
|
+
if (registration && agent === "claude" && projectDir) {
|
|
8717
8854
|
try {
|
|
8718
|
-
await assertClaudeProjectScope(homeDir, projectDir,
|
|
8855
|
+
await assertClaudeProjectScope(homeDir, projectDir, registration.name);
|
|
8719
8856
|
} catch (err) {
|
|
8720
8857
|
return { agent, skillInstalled: false, mcp: "failed", detail: err instanceof Error ? err.message : String(err) };
|
|
8721
8858
|
}
|
|
8722
8859
|
}
|
|
8723
8860
|
const source = await readFile9(join11(sourceDir, "topolo", "SKILL.md"), "utf8");
|
|
8724
8861
|
const skill = await installAgentSkill(agent, source, homeDir, projectDir, codexHome);
|
|
8725
|
-
if (!
|
|
8862
|
+
if (!registration) {
|
|
8726
8863
|
return { agent, skillPath: skill.path, skillInstalled: skill.installed, mcp: "skipped" };
|
|
8727
8864
|
}
|
|
8728
8865
|
try {
|
|
8729
|
-
await registerAgentConfig(agent, mcp, homeDir,
|
|
8730
|
-
const installed = (await readAgentRegistrations(agent, homeDir, projectDir, codexHome)).find((
|
|
8731
|
-
const expectedEnvironment = registrationEnvironment(agent,
|
|
8866
|
+
await registerAgentConfig(agent, mcp, homeDir, registration, projectDir, codexHome);
|
|
8867
|
+
const installed = (await readAgentRegistrations(agent, homeDir, projectDir, codexHome)).find((entry) => entry.name === registration.name);
|
|
8868
|
+
const expectedEnvironment = registrationEnvironment(agent, registration);
|
|
8732
8869
|
if (!installed || installed.command !== mcp.command || JSON.stringify(installed.args) !== JSON.stringify(mcp.args) || Object.entries(expectedEnvironment).some(([key, value]) => installed.env[key] !== value)) {
|
|
8733
|
-
throw new Error(`Host configuration did not retain the ${
|
|
8870
|
+
throw new Error(`Host configuration did not retain the ${registration.name} MCP registration.`);
|
|
8734
8871
|
}
|
|
8735
8872
|
return { agent, skillPath: skill.path, skillInstalled: skill.installed, mcp: "config-file" };
|
|
8736
8873
|
} catch (err) {
|
|
@@ -8767,7 +8904,8 @@ function resolveTargets(opts) {
|
|
|
8767
8904
|
function registerSkillsCommand(program2) {
|
|
8768
8905
|
const skills = program2.command("skills").description("Install the Topolo agent skill + MCP server into supported agent hosts");
|
|
8769
8906
|
skills.command("install").description("Copy the skill file and register the topolo MCP server (credential-free)").option("--claude", "Target Claude Code only").option("--codex", "Target Codex only").option("--agent <host...>", "Target one or more hosts by id (for example cursor gemini)").option("--all", "Configure every supported local host").option("--project <dir>", "Write project-scoped host files under <dir> where supported").option("--codex-home <dir>", "Use an explicit native Codex home instead of <agent-home>/.codex").option("--agent-home <dir>", "Target an alternate agent home (managed installs and clean-room verification)").option("--name <name>", "Register a named pinned MCP, such as topolo-acme").option("--context <context>", "Pin the named MCP to an exact context returned by topolo context ls").option("--installation <id>", "Bind the MCP runtime to a registered external-agent installation").option("--no-mcp", "Copy the skill file but do not register the MCP server").option("--json", "Output JSON", true).option("--no-json", "Output human-readable text").action(
|
|
8770
|
-
async (opts) => {
|
|
8907
|
+
async (opts, command) => {
|
|
8908
|
+
opts = withProgramContext(opts, command);
|
|
8771
8909
|
const mode = resolveOutputMode(opts.json);
|
|
8772
8910
|
try {
|
|
8773
8911
|
const { results, mcp } = await installAgentSurfaces({
|
|
@@ -8821,13 +8959,13 @@ async function installAgentSurfaces(opts) {
|
|
|
8821
8959
|
throw new Error("Bundled skill file not found. Reinstall the CLI: npm install -g @topolo/cli");
|
|
8822
8960
|
}
|
|
8823
8961
|
const mcp = resolveMcpCommand();
|
|
8824
|
-
const registration = resolveMcpRegistration(opts);
|
|
8825
8962
|
const target = resolveAgentHostTarget({ homeDir: opts.homeDir, projectDir: opts.project, codexHome: opts.codexHome });
|
|
8826
8963
|
const { homeDir, projectDir, codexHome } = target;
|
|
8964
|
+
const agents = resolveTargets({ ...opts, homeDir, project: projectDir, codexHome });
|
|
8965
|
+
const registration = opts.mcp === false ? null : resolveMcpRegistration(await withPinnedContext({ ...opts }, opts.readCredentialStore));
|
|
8827
8966
|
const results = [];
|
|
8828
|
-
for (const agent of
|
|
8967
|
+
for (const agent of agents) {
|
|
8829
8968
|
results.push(await installForAgent(agent, sourceDir, mcp, {
|
|
8830
|
-
withMcp: opts.mcp !== false,
|
|
8831
8969
|
target,
|
|
8832
8970
|
registration
|
|
8833
8971
|
}));
|
|
@@ -8835,7 +8973,8 @@ async function installAgentSurfaces(opts) {
|
|
|
8835
8973
|
return { results, mcp };
|
|
8836
8974
|
}
|
|
8837
8975
|
function registerSetupCommand(program2) {
|
|
8838
|
-
program2.command("setup").description("Install the Topolo skill and deterministic local MCP server for supported agent hosts").option("--claude", "Target Claude Code only").option("--codex", "Target Codex only").option("--agent <host...>", "Target one or more hosts by id").option("--all", "Configure every supported local host").option("--project <dir>", "Write project-scoped host files under <dir> where supported").option("--codex-home <dir>", "Use an explicit native Codex home instead of <agent-home>/.codex").option("--agent-home <dir>", "Target an alternate agent home (managed installs and clean-room verification)").option("--name <name>", "Register a named pinned MCP, such as topolo-acme").option("--context <context>", "Pin the named MCP to an exact context returned by topolo context ls").option("--installation <id>", "Bind the MCP runtime to a registered external-agent installation").option("--shared", "Opt in: register Claude Code and Codex against the shared local MCP hub (one process) instead of stdio").option("--port <port>", "With --shared: hub port on 127.0.0.1 (default 7337)").option("--import-stdio", "With --shared: also move every existing stdio topolo/topolo-* registration onto the hub").option("--no-service", "With --shared: do not install the launchd/systemd keep-alive unit").option("--json", "Output JSON", true).option("--no-json", "Output human-readable text").action(async (opts) => {
|
|
8976
|
+
program2.command("setup").description("Install the Topolo skill and deterministic local MCP server for supported agent hosts").option("--claude", "Target Claude Code only").option("--codex", "Target Codex only").option("--agent <host...>", "Target one or more hosts by id").option("--all", "Configure every supported local host").option("--project <dir>", "Write project-scoped host files under <dir> where supported").option("--codex-home <dir>", "Use an explicit native Codex home instead of <agent-home>/.codex").option("--agent-home <dir>", "Target an alternate agent home (managed installs and clean-room verification)").option("--name <name>", "Register a named pinned MCP, such as topolo-acme").option("--context <context>", "Pin the named MCP to an exact context returned by topolo context ls").option("--installation <id>", "Bind the MCP runtime to a registered external-agent installation").option("--shared", "Opt in: register Claude Code and Codex against the shared local MCP hub (one process) instead of stdio").option("--port <port>", "With --shared: hub port on 127.0.0.1 (default 7337)").option("--import-stdio", "With --shared: also move every existing stdio topolo/topolo-* registration onto the hub").option("--no-service", "With --shared: do not install the launchd/systemd keep-alive unit").option("--json", "Output JSON", true).option("--no-json", "Output human-readable text").action(async (opts, command) => {
|
|
8977
|
+
opts = withProgramContext(opts, command);
|
|
8839
8978
|
const mode = resolveOutputMode(opts.json);
|
|
8840
8979
|
try {
|
|
8841
8980
|
if (opts.shared) {
|
|
@@ -8905,7 +9044,7 @@ async function uninstallAgentSurfaces(options) {
|
|
|
8905
9044
|
const target = resolveAgentHostTarget({ homeDir: options.homeDir, projectDir: options.project, codexHome: options.codexHome });
|
|
8906
9045
|
const { homeDir, projectDir, codexHome } = target;
|
|
8907
9046
|
const name = options.name?.trim() || "topolo";
|
|
8908
|
-
resolveMcpRegistration({ name,
|
|
9047
|
+
resolveMcpRegistration({ name, context: "uninstall-validation" });
|
|
8909
9048
|
const removed = [];
|
|
8910
9049
|
for (const agent of resolveTargets({ ...options, homeDir, project: projectDir, codexHome })) {
|
|
8911
9050
|
const before = await readAgentRegistrations(agent, homeDir, projectDir, codexHome);
|
|
@@ -9162,7 +9301,8 @@ function definedEnvironment(environment) {
|
|
|
9162
9301
|
return Object.fromEntries(Object.entries(environment).filter((entry) => entry[1] !== void 0));
|
|
9163
9302
|
}
|
|
9164
9303
|
function registerDoctorCommand(program2) {
|
|
9165
|
-
program2.command("doctor").description("Verify installed agent surfaces, exact credential, and MCP protocol end to end").option("--name <name>", "Verify one installed MCP registration (defaults to topolo)").option("--context <context>", "Verify one exact personal or organization context").option("--environment <env>", "Override the context environment").option("--claude", "Verify Claude Code only").option("--codex", "Verify Codex only").option("--agent <host...>", "Verify one or more hosts by id").option("--all", "Verify every supported local host").option("--project <dir>", "Inspect project-scoped host files under <dir>").option("--agent-home <dir>", "Inspect an alternate agent home").option("--shared", "Verify the shared local MCP hub end to end for every hub identity (or --name)").option("--json", "Output JSON", true).option("--no-json", "Output human-readable text").action(async (opts) => {
|
|
9304
|
+
program2.command("doctor").description("Verify installed agent surfaces, exact credential, and MCP protocol end to end").option("--name <name>", "Verify one installed MCP registration (defaults to topolo)").option("--context <context>", "Verify one exact personal or organization context").option("--environment <env>", "Override the context environment").option("--claude", "Verify Claude Code only").option("--codex", "Verify Codex only").option("--agent <host...>", "Verify one or more hosts by id").option("--all", "Verify every supported local host").option("--project <dir>", "Inspect project-scoped host files under <dir>").option("--agent-home <dir>", "Inspect an alternate agent home").option("--shared", "Verify the shared local MCP hub end to end for every hub identity (or --name)").option("--json", "Output JSON", true).option("--no-json", "Output human-readable text").action(async (opts, command) => {
|
|
9305
|
+
opts = withProgramContext(opts, command);
|
|
9166
9306
|
const mode = resolveOutputMode(opts.json);
|
|
9167
9307
|
try {
|
|
9168
9308
|
if (opts.shared) {
|
|
@@ -9810,7 +9950,7 @@ function createMcpHubHandler(options) {
|
|
|
9810
9950
|
const log = options.log ?? (() => void 0);
|
|
9811
9951
|
const handlers = /* @__PURE__ */ new Map();
|
|
9812
9952
|
function handlerFor(identity, agentName) {
|
|
9813
|
-
const key = JSON.stringify([identity.name, identity.environment, identity.context
|
|
9953
|
+
const key = JSON.stringify([identity.name, identity.environment, identity.context, agentName]);
|
|
9814
9954
|
let handler = handlers.get(key);
|
|
9815
9955
|
if (!handler) {
|
|
9816
9956
|
handler = options.createIdentityHandler(identity, agentName);
|
|
@@ -9961,7 +10101,7 @@ async function startMcpHub(options) {
|
|
|
9961
10101
|
|
|
9962
10102
|
// src/commands/mcp.ts
|
|
9963
10103
|
var createLocalIdentityHandler = (identity, agentName) => createTopoloMcpLocalHandler({
|
|
9964
|
-
identity: { environment: identity.environment, context: identity.context
|
|
10104
|
+
identity: { environment: identity.environment, context: identity.context },
|
|
9965
10105
|
agentName
|
|
9966
10106
|
});
|
|
9967
10107
|
var defaultServeDependencies = () => ({
|
|
@@ -10027,10 +10167,18 @@ try {
|
|
|
10027
10167
|
var environmentWasStated = Boolean(inheritedEnvironment) || environmentArgSeen;
|
|
10028
10168
|
program.name("topolo").description(
|
|
10029
10169
|
"Use Topolo from your terminal: sign in, find your apps, and run actions.\n\nQuick start (copy a command, then press Enter):\n topolo auth login # Sign in with your browser\n topolo whoami # See who you are signed in as\n topolo apps --no-json # List your apps as a readable table\n topolo auth --help # See sign-in commands\n topolo help apps # See app commands and options"
|
|
10030
|
-
).version(CLI_VERSION).option("--env <environment>", "Topolo environment: production, staging, development, or local").addHelpText("beforeAll", '\nEvery command starts with "topolo". Running "topolo" alone prints help; it does not open an interactive prompt.\n').addHelpText("afterAll", "\nHelp notation: [brackets] mean optional; <angle brackets> mark a value to replace. Do not type the brackets.");
|
|
10170
|
+
).version(CLI_VERSION).option("--env <environment>", "Topolo environment: production, staging, development, or local").option("--context <context>", "Topolo context to act as: personal, org:<id>, or a signed-in organization slug or name (or set TOPOLO_CONTEXT)").addHelpText("beforeAll", '\nEvery command starts with "topolo". Running "topolo" alone prints help; it does not open an interactive prompt.\n').addHelpText("afterAll", "\nHelp notation: [brackets] mean optional; <angle brackets> mark a value to replace. Do not type the brackets.");
|
|
10031
10171
|
program.hook("preAction", async (thisCommand, actionCommand) => {
|
|
10032
10172
|
const opts = thisCommand.opts();
|
|
10033
10173
|
if (opts.env) process.env.TOPOLO_ENV = normalizeEnvironment(opts.env);
|
|
10174
|
+
if (opts.context !== void 0) {
|
|
10175
|
+
const context = opts.context.trim();
|
|
10176
|
+
if (!context || /[\r\n]/.test(context)) {
|
|
10177
|
+
process.stderr.write("--context must be a non-empty single-line identifier.\n");
|
|
10178
|
+
process.exit(2);
|
|
10179
|
+
}
|
|
10180
|
+
process.env.TOPOLO_CONTEXT = context;
|
|
10181
|
+
}
|
|
10034
10182
|
if (opts.env || environmentWasStated) return;
|
|
10035
10183
|
for (let node = actionCommand; node; node = node.parent) {
|
|
10036
10184
|
if (node.name() === "agent-packs") return;
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@topolo/cli",
|
|
3
3
|
"packageManager": "pnpm@10.17.1",
|
|
4
|
-
"version": "0.13.
|
|
5
|
-
"topoloPublishedVersion": "0.13.
|
|
4
|
+
"version": "0.13.16",
|
|
5
|
+
"topoloPublishedVersion": "0.13.16",
|
|
6
6
|
"description": "Public command-line interface for Topolo customer organizations, developers, and agents.",
|
|
7
7
|
"license": "UNLICENSED",
|
|
8
8
|
"author": "Topolo",
|
package/skills/topolo/SKILL.md
CHANGED
|
@@ -67,8 +67,16 @@ TOPOLO_CONTEXT=org:org_acme topolo --env production whoami --json
|
|
|
67
67
|
TOPOLO_CONTEXT=org:org_acme topolo --env production actions --service brand --json
|
|
68
68
|
```
|
|
69
69
|
|
|
70
|
-
|
|
71
|
-
|
|
70
|
+
`topolo --context <key> <command>` pins one command the same way. The CLI keeps
|
|
71
|
+
this pin through credential refresh and never changes the stored default
|
|
72
|
+
context. A missing pinned credential does not select another context.
|
|
73
|
+
|
|
74
|
+
There is no implicit default: when several contexts are signed in for an
|
|
75
|
+
environment, any command or MCP without `--context`/`TOPOLO_CONTEXT` refuses
|
|
76
|
+
("the context is no longer safe to assume"). Pin the context; do not run
|
|
77
|
+
`topolo context use` to work around the refusal. `topolo auth login` never moves
|
|
78
|
+
the stored default and prints which user and organization were approved; check
|
|
79
|
+
that line, because the browser may approve a different account than intended.
|
|
72
80
|
|
|
73
81
|
For concurrent agents or multiple MCP connections, never coordinate by changing
|
|
74
82
|
one shared active context. Register each pinned context through the CLI so it
|
|
@@ -79,6 +87,8 @@ topolo setup --name topolo-acme --context org:org_acme --env production
|
|
|
79
87
|
topolo doctor --name topolo-acme
|
|
80
88
|
```
|
|
81
89
|
|
|
90
|
+
Every registration setup writes pins `TOPOLO_ENV` and `TOPOLO_CONTEXT`; without
|
|
91
|
+
`--context` it pins the only signed-in context and refuses when there are several.
|
|
82
92
|
The context selector accepts an
|
|
83
93
|
exact key returned by `topolo context ls --json` (such as `personal` or
|
|
84
94
|
`org:<orgId>`) or a unique stored organization id, slug, or name. Each process
|