toolstile 0.3.0 → 0.4.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.
package/README.md CHANGED
@@ -165,7 +165,7 @@ values) and prints a ready-to-post link. To post the text report instead, add
165
165
 
166
166
  ## Connect your agents to Toolstile
167
167
 
168
- If you use [Toolstile](https://toolstile.com), the CLI can also sign in and set
168
+ If you use [Toolstile](https://www.toolstile.com), the CLI can also sign in and set
169
169
  up the agents on a machine for you:
170
170
 
171
171
  ```sh
@@ -211,7 +211,7 @@ dashboard.
211
211
 
212
212
  | Option | Command | Meaning |
213
213
  | --- | --- | --- |
214
- | `--server <url>` | `auth login` | Toolstile address (default `https://toolstile.com`, or `TOOLSTILE_URL`) |
214
+ | `--server <url>` | `auth login` | Toolstile address (default `https://app.toolstile.com`, or `TOOLSTILE_URL`) |
215
215
  | `--no-browser` | `auth login` | Do not try to open a browser on this machine |
216
216
  | `--enable <agents>` | `mcp setup` | Turn these on without prompting (comma-separated) |
217
217
  | `--disable <agents>` | `mcp setup` | Turn these off without prompting |
@@ -229,7 +229,7 @@ const { grade, findings } = audit();
229
229
  ## About
230
230
 
231
231
  Built by [Akshit Kr Nagpal](https://akshit.io) as part of
232
- [Toolstile](https://toolstile.com), an MCP gateway that keeps upstream
232
+ [Toolstile](https://www.toolstile.com), an MCP gateway that keeps upstream
233
233
  credentials off your machines and lets you choose which tools each agent can use.
234
234
  The audit works on its own and does not require a Toolstile account.
235
235
 
package/dist/api.d.ts CHANGED
@@ -20,6 +20,8 @@ export interface Account {
20
20
  id: string;
21
21
  label: string;
22
22
  } | null;
23
+ /** The gateway URL agents use; it can be on another host than this API. */
24
+ mcpUrl?: string;
23
25
  }
24
26
  export interface DeviceCode {
25
27
  device_code: string;
package/dist/cli.js CHANGED
@@ -68,7 +68,7 @@ login prints a link and a code. Open the link on any device with a browser
68
68
  reach this machine, so it works over SSH and on servers without a browser.
69
69
 
70
70
  Options
71
- --server <url> Toolstile address (default https://toolstile.com, or TOOLSTILE_URL)
71
+ --server <url> Toolstile address (default https://app.toolstile.com, or TOOLSTILE_URL)
72
72
  --no-browser Do not try to open a browser on this machine
73
73
  `;
74
74
  const MCP_HELP = `toolstile mcp: connect the agents on this machine to Toolstile
@@ -23,6 +23,8 @@ export declare class UserError extends Error {
23
23
  }
24
24
  /** HTTPS, except a loopback server for local development. */
25
25
  export declare function normalizeServer(value: string): string;
26
+ /** A gateway URL the server reported: HTTPS (or loopback HTTP) with no credentials, else undefined. */
27
+ export declare function gatewayUrl(value: unknown): string | undefined;
26
28
  export declare function login(ctx: Context, options: {
27
29
  server?: string;
28
30
  browser: boolean;
package/dist/commands.js CHANGED
@@ -20,13 +20,31 @@ export function normalizeServer(value) {
20
20
  url = new URL(value);
21
21
  }
22
22
  catch {
23
- throw new UserError(`--server must be a URL, for example https://toolstile.com`);
23
+ throw new UserError(`--server must be a URL, for example https://app.toolstile.com`);
24
24
  }
25
25
  const loopback = /^(localhost|127\.\d+\.\d+\.\d+|\[::1\])$/.test(url.hostname);
26
26
  if (url.protocol !== "https:" && !(loopback && url.protocol === "http:"))
27
27
  throw new UserError("--server must use https://");
28
28
  return url.origin;
29
29
  }
30
+ /** A gateway URL the server reported: HTTPS (or loopback HTTP) with no credentials, else undefined. */
31
+ export function gatewayUrl(value) {
32
+ if (typeof value !== "string")
33
+ return undefined;
34
+ let url;
35
+ try {
36
+ url = new URL(value);
37
+ }
38
+ catch {
39
+ return undefined;
40
+ }
41
+ const loopback = /^(localhost|127\.\d+\.\d+\.\d+|\[::1\])$/.test(url.hostname);
42
+ if (url.username || url.password || url.search || url.hash)
43
+ return undefined;
44
+ if (url.protocol !== "https:" && !(loopback && url.protocol === "http:"))
45
+ return undefined;
46
+ return url.href;
47
+ }
30
48
  function defaultOpenBrowser(ctx) {
31
49
  return (url) => {
32
50
  // Over SSH or without a display the URL and code are enough; nothing to open.
@@ -180,6 +198,8 @@ export async function setup(ctx, options) {
180
198
  const both = enable.find((c) => disable.includes(c));
181
199
  if (both)
182
200
  throw new UserError(`${CLIENT_NAMES[both]} is in both --enable and --disable.`);
201
+ // The gateway can be on another host; only an HTTPS URL from this server is used.
202
+ const endpoint = gatewayUrl(account.mcpUrl) ?? `${state.server}/mcp`;
183
203
  const workspace = await chooseWorkspace(ctx, state, account.workspaces, options.workspace);
184
204
  state.workspace = { id: workspace.id, slug: workspace.slug, name: workspace.name };
185
205
  saveState(state, stateEnv(ctx));
@@ -254,6 +274,10 @@ export async function setup(ctx, options) {
254
274
  const want = wanted.has(target.client);
255
275
  const link = state.agents[target.client];
256
276
  if (want) {
277
+ if (live(target.client) && link && link.endpoint !== endpoint) {
278
+ link.endpoint = endpoint;
279
+ saveState(state, stateEnv(ctx));
280
+ }
257
281
  if (!live(target.client)) {
258
282
  if (link)
259
283
  await removeRemote(link).catch(() => undefined);
@@ -263,6 +287,7 @@ export async function setup(ctx, options) {
263
287
  id: created.id,
264
288
  workspaceId: workspace.id,
265
289
  server: state.server,
290
+ endpoint,
266
291
  label: created.label,
267
292
  token: created.token,
268
293
  };
@@ -324,7 +349,7 @@ export async function setup(ctx, options) {
324
349
  }
325
350
  if ([...wanted].length) {
326
351
  print(ctx);
327
- print(ctx, `Choose which MCP servers they can use: ${state.server}/app/${workspace.slug}/access`);
352
+ print(ctx, `Choose which MCP servers they can use: ${state.server}/${workspace.slug}/access`);
328
353
  }
329
354
  return manual.length ? 1 : 0;
330
355
  }
@@ -338,7 +363,7 @@ export async function proxy(ctx, agent) {
338
363
  throw new UserError(`${CLIENT_NAMES[client]} is not enabled on this machine. Run toolstile mcp setup.`);
339
364
  await runProxy({
340
365
  // The token goes only to the server that issued it, whatever the environment says.
341
- endpoint: `${link.server}/mcp`,
366
+ endpoint: link.endpoint ?? `${link.server}/mcp`,
342
367
  token: link.token,
343
368
  input: ctx.stdin,
344
369
  output: ctx.stdout,
package/dist/report.js CHANGED
@@ -179,7 +179,7 @@ export function renderReport(r, home, s, redacted = false, columns = 80) {
179
179
  lines.push(s.dim(wrap("Posting this? Run with --redact to hide your paths, or --card for an image with only the grade.", width)));
180
180
  else
181
181
  lines.push(s.dim("Share an image with only the grade: npx toolstile audit --card"));
182
- lines.push(s.dim(wrap("Keep credentials off your machines and control each agent's tools: https://toolstile.com/?ref=cli", width)), "");
182
+ lines.push(s.dim(wrap("Keep credentials off your machines and control each agent's tools: https://www.toolstile.com/?ref=cli", width)), "");
183
183
  return lines.join("\n");
184
184
  }
185
185
  /** JSON output for scripts. Paths are included unless redacted; secret values never are. */
package/dist/state.d.ts CHANGED
@@ -1,11 +1,15 @@
1
1
  import type { ClientId } from "./types.js";
2
- export declare const DEFAULT_SERVER = "https://toolstile.com";
2
+ export declare const DEFAULT_SERVER = "https://app.toolstile.com";
3
+ /** Toolstile's gateway, on its own host since the app moved off the bare domain. */
4
+ export declare const DEFAULT_MCP_URL = "https://mcp.toolstile.com/mcp";
3
5
  export interface AgentLink {
4
6
  /** The agent's authorization ID in Toolstile, valid in one workspace. */
5
7
  id: string;
6
8
  workspaceId: string;
7
9
  /** The Toolstile origin that issued the token; the proxy sends it nowhere else. */
8
10
  server: string;
11
+ /** That server's gateway URL, as it reported it; `${server}/mcp` when absent. */
12
+ endpoint?: string;
9
13
  label: string;
10
14
  /** The agent's own gateway token; only `toolstile mcp proxy` reads it. */
11
15
  token: string;
package/dist/state.js CHANGED
@@ -1,7 +1,11 @@
1
1
  import { chmodSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { join } from "node:path";
4
- export const DEFAULT_SERVER = "https://toolstile.com";
4
+ export const DEFAULT_SERVER = "https://app.toolstile.com";
5
+ /** Toolstile's gateway, on its own host since the app moved off the bare domain. */
6
+ export const DEFAULT_MCP_URL = "https://mcp.toolstile.com/mcp";
7
+ /** Where Toolstile served the app and gateway before; it now only redirects. */
8
+ const LEGACY_SERVER = "https://toolstile.com";
5
9
  /** `TOOLSTILE_CONFIG_DIR`, else the platform's per-user config folder. */
6
10
  export function stateDir({ home = homedir(), env = process.env, platform = process.platform } = {}) {
7
11
  if (env.TOOLSTILE_CONFIG_DIR)
@@ -28,7 +32,7 @@ export function loadState(opts) {
28
32
  }
29
33
  try {
30
34
  const data = JSON.parse(text);
31
- return { ...fallback, ...data, agents: data.agents ?? {} };
35
+ return moveLegacyServer({ ...fallback, ...data, agents: data.agents ?? {} });
32
36
  }
33
37
  catch {
34
38
  throw new Error(`${statePath(opts)} is not valid JSON. Delete it and run toolstile auth login again.`);
@@ -49,3 +53,16 @@ export function saveState(state, opts) {
49
53
  rmSync(temp, { force: true });
50
54
  }
51
55
  }
56
+ /** Sign-ins and agents saved for the old bare-domain address keep working on the new hosts. */
57
+ function moveLegacyServer(state) {
58
+ if (state.server === LEGACY_SERVER)
59
+ state.server = DEFAULT_SERVER;
60
+ if (state.session?.server === LEGACY_SERVER)
61
+ state.session.server = DEFAULT_SERVER;
62
+ for (const link of Object.values(state.agents))
63
+ if (link?.server === LEGACY_SERVER) {
64
+ link.server = DEFAULT_SERVER;
65
+ link.endpoint ??= DEFAULT_MCP_URL;
66
+ }
67
+ return state;
68
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toolstile",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Audit the MCP server configs your AI agents use for plaintext secrets, unpinned packages, auto-approved tools and duplicated setups, and connect those agents to Toolstile without putting tokens in their configs.",
5
5
  "keywords": [
6
6
  "mcp",