nylorun 0.11.1-beta → 0.12.0-beta

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/CHANGELOG.md CHANGED
@@ -1,5 +1,55 @@
1
1
  # nylorun
2
2
 
3
+ ## 0.12.0-beta
4
+
5
+ ### Major Changes
6
+
7
+ - 50e2f7e: **Protocol 10: MCP credentials come from a session's vaults only.** Nylorun no longer signs the installation in to MCP servers with OAuth and no longer asks a credential resolver. Upgrade every package together; MIGRATION.md has the details.
8
+
9
+ - **Breaking (`@nylorun/runtime`): the MCP OAuth connect is gone.** `POST /v1/tenant/vaults/{vaultId}/oauth/start` and `GET /v1/oauth/callback` answer `404`. The vault credential type `oauth` and its refresh are gone: a credential is a `bearer` token or a `headers` map bound to a URL. Migration `0016_mcp_oauth_removed` drops the table of pending connects and deletes every `oauth` credential, writing one audit row each (actor `migration`); the Runtime logs `oauth_credential_removed` once for each, naming its vault, id and URL.
10
+ - **Breaking (`@nylorun/runtime`): the credential resolver is gone.** The gateway no longer asks the operator's resolver for a person's credential when the session's vaults hold none. A process that still sets a `NYLORUN_RESOLVER_*` variable logs `resolver_removed` and ignores it. Keep a person's own keys in their user vault and attach it to their sessions (`vaultIds`). `TenantConfig.resolver`, `TenantConfig.publicUrl`, `TenantConfig.vaultFetch`, `startEphemeralRuntime({ resolver })`, the `ResolverConfig` export and `VaultService`'s `fetch` option are removed; `NYLORUN_PUBLIC_URL` still sets the protected resource metadata's `resource`.
11
+ - **Breaking (`@nylorun/core`):** `PROTOCOL_VERSION` is 10 and `HOST_PROTOCOL` 4–10. `StartOAuthRequest`, `StartOAuthResponse`, the `oauth` variants of `CreateCredentialRequest` and `RotateCredentialRequest`, `oauth` in `CredentialInfo.type` and `CredentialInfo.expiresAt` are removed, and `ERROR_CODES` drops `oauth_client_required`, `oauth_state_invalid` and `oauth_failed`. `@nylorun/agents` and `@nylorun/cli` send protocol 10.
12
+ - **Breaking (`@nylorun/admin`):** `admin.vaults` loses its OAuth start method.
13
+ - **Breaking (`nylorun`):** the `connect` subcommand of `nylorun mcp` is removed (`nylorun mcp inspect` lists a server's tools instead), and the gateway's Compose service no longer passes the `NYLORUN_RESOLVER_*` variables.
14
+ - `@nylorun/studio`: the Credentials page loses the OAuth type, the Expires column and the OAuth connect hint.
15
+
16
+ ### Minor Changes
17
+
18
+ - da93711: **Preview an MCP server's tools** (R2b C12). MIGRATION.md (protocol 10, "Previewing a server's tools") has the details.
19
+
20
+ - `@nylorun/core`: `McpPreviewRequestSchema` and `McpPreviewSchema` (`McpPreviewTool`), and the error code `mcp_preview_failed`.
21
+ - `@nylorun/runtime`: `POST /v1/tenant/mcp/preview` (the Management API) connects to a remote MCP server with the installation vault's credential for its URL (headers and `via`, no identity header), under the Host's address policy and within 15 s, and answers its server info, instructions, tools (model names, annotations, schema sizes) and renames; a `401` is `authRequired`, with the server's RFC 9728 protected-resource metadata. It runs in the keys service (`Keys.previewMcp`), which holds the plaintext, and calls no tool. Both OpenAPI documents list it.
22
+ - `@nylorun/admin`: `admin.mcp.preview({ url, type?, name?, vaultId? })`.
23
+ - `nylorun`: `nylorun mcp inspect <url> [--server <name>] [--vault <id>] [--sse] [--json]` prints the running Tenant's preview: a table of tools, the renames, or that the server needs a person's sign-in. Its `connect` subcommand still says it was removed, and now points to `inspect`.
24
+ - `@nylorun/studio`: **Preview tools** on each credential of the Credentials page lists the tools behind its URL.
25
+
26
+ ### Patch Changes
27
+
28
+ - fed5e58: **Docs: reaching a person's accounts** (R2b C5). The READMEs describe MCP credentials as protocol 10 has them (a `bearer` token or a `headers` map per URL, with `via` and an identity header for an MCP gateway), and point to "MCP servers and HTTP tools" in DEPLOYMENT.md: the operator's flow from credential to preview, tool settings, deferral, stored results and the error codes a model sees, with gateway recipes for Arcade, ToolHive, Obot and Nylorun Cloud and the proxy pattern for gateways that mint per person. `HttpToolTarget.credential`'s doc (`@nylorun/core`) no longer names the removed credential resolver.
29
+ - Pin core to the tested release.
30
+ - Pin runtime to the tested release.
31
+ - Pin studio to the tested release.
32
+ - Updated dependencies [50e2f7e]
33
+ - Updated dependencies [e0e39ff]
34
+ - Updated dependencies [fed5e58]
35
+ - Updated dependencies [18f9a2f]
36
+ - Updated dependencies [da93711]
37
+ - Updated dependencies [90a817d]
38
+ - Updated dependencies [b8d10cb]
39
+ - @nylorun/core@0.17.0-beta
40
+
41
+ ## 0.11.2-beta
42
+
43
+ ### Patch Changes
44
+
45
+ - 6711320: `/ready` no longer checks S2: its `checks` cover the listener, the Tenant, Postgres and Restate, and an unreachable S2 leaves it `200`. S2 only serves API listeners (history, SSE, AG-UI and A2A), so an outage degrades those reads and never makes the Runtime unready. S2's reachability stays in the Tenant's status (`GET /v1/tenant`, `streams.reachable`).
46
+ - Pin core to the tested release.
47
+ - Pin runtime to the tested release.
48
+ - Pin studio to the tested release.
49
+ - Updated dependencies [cf5eb9c]
50
+ - Updated dependencies [2ed5fe0]
51
+ - @nylorun/core@0.16.0-beta
52
+
3
53
  ## 0.11.1-beta
4
54
 
5
55
  ### Patch Changes
package/README.md CHANGED
@@ -49,7 +49,7 @@ nylorun sandbox rm <id> [--tenant <name>] # delete a sandbox and its files
49
49
  nylorun key put <id> [--management] [--tenant <name>] # create or rotate the Tenant's application key <id> (--management: a management key); prints it once
50
50
  nylorun key list [--tenant <name>] [--json] # the Tenant's keys: id, role, when issued
51
51
  nylorun key rm <id> [--tenant <name>] # delete a key: it stops working at once
52
- nylorun mcp connect <url> --server <name> [--vault <id>] [--client-id <id>] [--tenant <name>] [--no-open] # sign the Tenant in to a remote MCP server with OAuth
52
+ nylorun mcp inspect <url> [--server <name>] [--vault <id>] [--sse] [--tenant <name>] [--json] # a remote MCP server's tools, with the installation vault's credential
53
53
  nylorun doctor [--json] # prerequisites and the Tenant's health
54
54
  nylorun telemetry [status|enable|disable] # Studio's anonymous usage analytics
55
55
  nylorun sandbox enable --context <name> [--tenant <name>] [--host-address <ip>] [--bind-address <ip>] [--no-pull]
@@ -60,7 +60,9 @@ nylorun sandbox status [--tenant <name>] [--json] # pods on a Kubernetes conte
60
60
  `up` and `down` are the Docker Compose spellings of `start` and `stop`: `down`
61
61
  stops the containers and keeps the volumes, so no Tenant data is lost. Removed
62
62
  commands exit 2 naming the replacement: `nylorun dev`, `nylorun configure`,
63
- `nylorun status --env` and `nylorun doctor sandbox`.
63
+ `nylorun status --env`, `nylorun doctor sandbox` and the MCP OAuth `connect`
64
+ subcommand of `nylorun mcp` (protocol 10: add a key or a gateway credential to a
65
+ vault instead, and preview the server with `nylorun mcp inspect`).
64
66
 
65
67
  ## Tenants
66
68
 
@@ -186,17 +188,33 @@ key's id, role and when it was issued, never the keys; `nylorun key rm <id>`
186
188
  deletes one. Ids match `^[a-z][a-z0-9-]{0,31}$`; `studio` belongs to Studio and
187
189
  `bootstrap` to `NYLORUN_MANAGEMENT_KEY_FILE`, and both are refused. Putting an
188
190
  id that holds the other role is refused. Give each app server its own
189
- application key. `nylorun sandbox` and `nylorun mcp` use the linked project's
190
- keys, or the keys `cli` and `cli-management` they put once and keep in
191
+ application key. `nylorun sandbox` and `nylorun mcp` use the linked project's keys, or the keys
192
+ `cli` and `cli-management` they put once and keep in
191
193
  `<Host root>/cli-credentials.json` (mode 0600).
192
194
 
193
- `nylorun mcp connect <url> --server <name>` signs the running Tenant in to a
194
- remote MCP server that uses OAuth: it opens the server's sign-in page in the
195
- browser and waits until the Runtime has stored the credential in the
196
- installation vault `mcp` (created if needed; `--vault <id>` picks another),
197
- which sessions attach with `vaultIds`. Pass `--client-id` when the server does
198
- not let clients register themselves. See
199
- [Connecting a remote MCP server with OAuth](../DEPLOYMENT.md#connecting-a-remote-mcp-server-with-oauth).
195
+ ## MCP servers
196
+
197
+ An agent names its MCP servers by URL; the operator gives each URL a vault
198
+ credential, a `bearer` token or a `headers` map, with `via` and an identity
199
+ header when an MCP gateway holds each person's sign-in. Add it on Studio's
200
+ Credentials page or with `@nylorun/admin` (no `nylorun` command adds one); the
201
+ whole flow, with gateway recipes, is in
202
+ [DEPLOYMENT.md](../DEPLOYMENT.md#mcp-servers-and-http-tools).
203
+
204
+ `nylorun mcp inspect <url>` shows what a remote MCP server offers before an
205
+ agent names it, through the running Tenant's Management API
206
+ (`POST /v1/tenant/mcp/preview`, with the management key). The Runtime connects
207
+ with the installation vault's credential for `<url>` (`--vault <id>` picks the
208
+ vault when several hold one), lists the tools within 15 s and calls none. It
209
+ prints a table of each tool's model name, input schema size, hints
210
+ (`read-only`, `destructive`, `idempotent`) and description, then the tools
211
+ renamed for the model. `--server` is the server name your manifest uses (default:
212
+ the host's name, `linear` for `mcp.linear.app`); `--sse` speaks the older SSE
213
+ transport; `--json` prints the Runtime's answer. A server that answers `401`
214
+ needs a credential: it says so, and names the server's sign-in from its
215
+ protected-resource metadata, since such a server needs a key in a vault or an
216
+ MCP gateway that holds each person's sign-in. A server it cannot list exits 1 with
217
+ the reason (`mcp.unreachable` for an address the Runtime refuses).
200
218
 
201
219
  ## The containers
202
220
 
package/dist/cli.js CHANGED
@@ -52,14 +52,14 @@ async function main() {
52
52
  process.exitCode = await sandboxCommand(args, baselineEnv());
53
53
  return;
54
54
  }
55
- if (command === "mcp") {
56
- process.exitCode = await mcpCommand(args, baselineEnv());
57
- return;
58
- }
59
55
  if (command === "key") {
60
56
  process.exitCode = await keyCommand(args, baselineEnv());
61
57
  return;
62
58
  }
59
+ if (command === "mcp") {
60
+ process.exitCode = await mcpCommand(args, baselineEnv());
61
+ return;
62
+ }
63
63
  if (isStackCommand(command)) {
64
64
  process.exitCode = await stackCommand(command, args, baselineEnv());
65
65
  return;
@@ -137,7 +137,7 @@ export interface StackEndpoints {
137
137
  /** This call started the Runtime (it was not running before). */
138
138
  started: boolean;
139
139
  }
140
- /** The running Tenant's APIs and their keys (`nylorun sandbox`, `nylorun mcp`). */
140
+ /** The running Tenant's APIs and their keys (`nylorun sandbox`). */
141
141
  export interface TenantApi {
142
142
  /** The Tenant's name. */
143
143
  name: string;
@@ -9,7 +9,7 @@
9
9
  * secrets, UID/GID, the Host root, the images) comes from `docker/.env`.
10
10
  *
11
11
  * The Tenant's state is its Postgres database, executed through Restate, with its
12
- * history in s2-lite; the Runtime's /ready checks all three. Postgres initialises the
12
+ * history in s2-lite; the Runtime's /ready checks Postgres and Restate. Postgres initialises the
13
13
  * database with C collation (`--locale=C`) and runs with `wal_level=logical`: the stream
14
14
  * relay feeds s2-lite from the record over logical replication (Durable Streams), and
15
15
  * `max_slot_wal_keep_size` caps the WAL a stuck relay can hold. The Runtime creates the
@@ -10,7 +10,7 @@ import { PINNED_IMAGES } from "./images.js";
10
10
  * secrets, UID/GID, the Host root, the images) comes from `docker/.env`.
11
11
  *
12
12
  * The Tenant's state is its Postgres database, executed through Restate, with its
13
- * history in s2-lite; the Runtime's /ready checks all three. Postgres initialises the
13
+ * history in s2-lite; the Runtime's /ready checks Postgres and Restate. Postgres initialises the
14
14
  * database with C collation (`--locale=C`) and runs with `wal_level=logical`: the stream
15
15
  * relay feeds s2-lite from the record over logical replication (Durable Streams), and
16
16
  * `max_slot_wal_keep_size` caps the WAL a stuck relay can hold. The Runtime creates the
@@ -131,8 +131,8 @@ ${restateUi ? RESTATE_UI : RESTATE_CLOSED} healthcheck:
131
131
  volumes:
132
132
  - s2-lite:/home/nonroot
133
133
  networks: [store]
134
- # The s2 image has no shell or HTTP client, so it has no health check;
135
- # the Runtime's /ready covers it.
134
+ # The s2 image has no shell or HTTP client, so it has no health check.
135
+ # Its reachability is in the Tenant's status (GET /v1/tenant, streams.reachable).
136
136
  restart: unless-stopped
137
137
 
138
138
  rustfs: # the Object store (D35); not published, only the runtime and the gateway hold its credential
@@ -175,11 +175,7 @@ ${restateUi ? RESTATE_UI : RESTATE_CLOSED} healthcheck:
175
175
  NYLORUN_OBJECT_STORE_ACCESS_KEY: nylorun
176
176
  NYLORUN_OBJECT_STORE_SECRET_KEY: \${NYLORUN_OBJECT_STORE_SECRET_KEY:?run nylorun start}
177
177
  # MCP servers and HTTP tools on this machine: \`localhost\` in their URLs means the Docker host.
178
- NYLORUN_ENDPOINT_LOOPBACK: docker-host
179
- # Your credential resolver, for people's own MCP credentials (DEPLOYMENT.md, Credentials);
180
- # unset by default. Set both in the shell that runs \`nylorun start\`.
181
- NYLORUN_RESOLVER_URL: \${NYLORUN_RESOLVER_URL:-}
182
- NYLORUN_RESOLVER_TOKEN: \${NYLORUN_RESOLVER_TOKEN:-}${sandboxes ? SANDBOXES_GATEWAY_ENV : ""}
178
+ NYLORUN_ENDPOINT_LOOPBACK: docker-host${sandboxes ? SANDBOXES_GATEWAY_ENV : ""}
183
179
  extra_hosts:
184
180
  host.docker.internal: host-gateway # model servers, MCP servers and HTTP tools on this machine
185
181
  volumes:
@@ -10,12 +10,12 @@ export declare function stackCommand(name: string, args: readonly string[], env:
10
10
  /** Entry for `nylorun sandbox ls|rm`. */
11
11
  export declare function sandboxCommand(args: readonly string[], env: Readonly<Record<string, string | undefined>>): Promise<number>;
12
12
  export { sandboxUsage } from "./sandbox.js";
13
- /** Entry for `nylorun mcp connect`. */
14
- export declare function mcpCommand(args: readonly string[], env: Readonly<Record<string, string | undefined>>): Promise<number>;
15
- export { mcpUsage } from "./mcp.js";
16
13
  /** Entry for `nylorun key put|list|rm`. */
17
14
  export declare function keyCommand(args: readonly string[], env: Readonly<Record<string, string | undefined>>): Promise<number>;
18
15
  export { keyUsage } from "./keys.js";
16
+ /** Entry for `nylorun mcp inspect`. */
17
+ export declare function mcpCommand(args: readonly string[], env: Readonly<Record<string, string | undefined>>): Promise<number>;
18
+ export { mcpUsage } from "./mcp.js";
19
19
  /** `nylorun studio`, landing on `next` when given, else on the Tenant's page. */
20
20
  export declare function studioCommand(args: readonly string[], env: Readonly<Record<string, string | undefined>>, options?: {
21
21
  next?: string;
@@ -3,8 +3,8 @@ import { createInterface } from "node:readline/promises";
3
3
  import { runStackCommand, runStudioCommand } from "./commands.js";
4
4
  import { spawnDocker } from "./docker.js";
5
5
  import { keyCommand as runKeyCommand } from "./keys.js";
6
- import { sandboxCommand as runSandboxCommand } from "./sandbox.js";
7
6
  import { mcpCommand as runMcpCommand } from "./mcp.js";
7
+ import { sandboxCommand as runSandboxCommand } from "./sandbox.js";
8
8
  import { pinnedVersion } from "./versions.js";
9
9
  import { loopbackPorts } from "./ports.js";
10
10
  export { ensureStack, isStackCommand, readStackStatus, stackUsage, studioLoginUrl, tenantStudioPath, STACK_SERVICES, } from "./commands.js";
@@ -86,16 +86,16 @@ export async function sandboxCommand(args, env) {
86
86
  return await runSandboxCommand(defaultStackDeps(env), args);
87
87
  }
88
88
  export { sandboxUsage } from "./sandbox.js";
89
- /** Entry for `nylorun mcp connect`. */
90
- export async function mcpCommand(args, env) {
91
- return await runMcpCommand(defaultStackDeps(env), args);
92
- }
93
- export { mcpUsage } from "./mcp.js";
94
89
  /** Entry for `nylorun key put|list|rm`. */
95
90
  export async function keyCommand(args, env) {
96
91
  return await runKeyCommand(defaultStackDeps(env), args);
97
92
  }
98
93
  export { keyUsage } from "./keys.js";
94
+ /** Entry for `nylorun mcp inspect`. */
95
+ export async function mcpCommand(args, env) {
96
+ return await runMcpCommand(defaultStackDeps(env), args);
97
+ }
98
+ export { mcpUsage } from "./mcp.js";
99
99
  /** `nylorun studio`, landing on `next` when given, else on the Tenant's page. */
100
100
  export async function studioCommand(args, env, options = {}) {
101
101
  return await runStudioCommand(args, defaultStackDeps(env), options);
@@ -1,8 +1,9 @@
1
+ import type { McpPreview } from "@nylorun/core/contracts";
1
2
  import { type StackDeps } from "./commands.js";
2
- export declare const mcpUsage = " mcp connect <url> --server <name> [--vault <id>] [--client-id <id>] [--tenant <name>] [--no-open]\n sign the Tenant in to a remote MCP server with OAuth; the credential goes to the\n installation vault mcp (or --vault), which sessions attach";
3
- /** The installation vault `connect` uses unless `--vault` names one. */
4
- export declare const MCP_VAULT_NAME = "mcp";
5
- /** How long `connect` waits for the browser sign-in: the Runtime keeps it for ten minutes. */
6
- export declare const CONNECT_TIMEOUT_MS: number;
7
- /** `nylorun mcp <connect> ...` */
3
+ export declare const mcpUsage = " mcp inspect <url> [--server <name>] [--vault <id>] [--sse] [--tenant <name>] [--json]\n list a remote MCP server's tools with the installation vault's\n credential for <url>: model names, schema sizes, renames, or\n that it needs a person's sign-in (the Runtime calls no tool)";
4
+ /** What the removed `connect` subcommand says since protocol 10. */
5
+ export declare const MCP_CONNECT_REMOVED = "The connect subcommand of nylorun mcp was removed with MCP OAuth (protocol 10): add the server's key to an installation vault as a bearer or headers credential (Studio's Credentials page, or @nylorun/admin), or reach a server that needs a person's sign-in through a gateway (a credential with via). See its tools with nylorun mcp inspect <url>.";
6
+ /** A preview as a person reads it: the server, a table of tools, the renames, or the sign-in hint. */
7
+ export declare function previewLines(preview: McpPreview): string[];
8
+ /** `nylorun mcp <inspect|connect> ...` */
8
9
  export declare function mcpCommand(deps: StackDeps, args: readonly string[]): Promise<number>;
package/dist/stack/mcp.js CHANGED
@@ -1,114 +1,142 @@
1
1
  /**
2
- * `nylorun mcp connect <url> --server <name>`: signs the running local Tenant in to a remote MCP
3
- * server with OAuth (F9 C2), through its Management API with a management key (`runningTenantApi`).
4
- * The Runtime's keys module runs discovery, registration and the code exchange and seals the
5
- * credential in an installation vault (`mcp` unless `--vault` names one); this command only
6
- * opens the browser and waits for the credential to appear. It never starts the Tenant.
2
+ * `nylorun mcp inspect <url>`: a remote MCP server's tools before an agent names it (R2b C12),
3
+ * through the running local Tenant's Management API (`POST /v1/tenant/mcp/preview`) with the
4
+ * Project link's management key, else the Host root's (`runningTenantApi`). It never starts the
5
+ * Tenant, and the Runtime never calls a tool. Its `connect` subcommand (MCP OAuth) was removed
6
+ * in protocol 10, and says so.
7
7
  */
8
- import { randomUUID } from "node:crypto";
9
8
  import { PROTOCOL_HEADER, PROTOCOL_VERSION } from "@nylorun/core/compatibility";
10
9
  import { CliError } from "../errors.js";
11
10
  import { parseStackFlags, runningTenantApi } from "./commands.js";
12
- export const mcpUsage = ` mcp connect <url> --server <name> [--vault <id>] [--client-id <id>] [--tenant <name>] [--no-open]
13
- sign the Tenant in to a remote MCP server with OAuth; the credential goes to the
14
- installation vault mcp (or --vault), which sessions attach`;
15
- /** The installation vault `connect` uses unless `--vault` names one. */
16
- export const MCP_VAULT_NAME = "mcp";
17
- /** How long `connect` waits for the browser sign-in: the Runtime keeps it for ten minutes. */
18
- export const CONNECT_TIMEOUT_MS = 10 * 60_000;
11
+ export const mcpUsage = ` mcp inspect <url> [--server <name>] [--vault <id>] [--sse] [--tenant <name>] [--json]
12
+ list a remote MCP server's tools with the installation vault's
13
+ credential for <url>: model names, schema sizes, renames, or
14
+ that it needs a person's sign-in (the Runtime calls no tool)`;
15
+ const INSPECT_USAGE = "nylorun mcp inspect <url> [--server <name>] [--vault <id>] [--sse] [--tenant <name>] [--json]";
16
+ /** What the removed `connect` subcommand says since protocol 10. */
17
+ export const MCP_CONNECT_REMOVED = "The connect subcommand of nylorun mcp was removed with MCP OAuth (protocol 10): add the server's key to an installation vault as a bearer or headers credential (Studio's Credentials page, or @nylorun/admin), or reach a server that needs a person's sign-in through a gateway (a credential with via). See its tools with nylorun mcp inspect <url>.";
18
+ /** The preview takes up to 15 s in the Runtime; the hop gets a margin. */
19
+ const PREVIEW_TIMEOUT_MS = 30_000;
20
+ const DESCRIPTION_CHARS = 60;
19
21
  const usageError = (message) => new CliError(message, 2);
20
- async function call(deps, api, method, path, body) {
21
- const response = await deps.fetch(`${api.runtimeUrl}${path}`, {
22
- method,
22
+ async function inspect(deps, args) {
23
+ const flags = parseStackFlags(args, { booleans: ["--json", "--sse"], values: ["--server", "--vault", "--tenant"] }, INSPECT_USAGE);
24
+ if (flags.rest.length !== 1)
25
+ throw usageError(`Usage: ${INSPECT_USAGE}`);
26
+ const url = flags.rest[0];
27
+ const name = flags.values.get("--server");
28
+ const vaultId = flags.values.get("--vault");
29
+ const tenant = flags.values.get("--tenant");
30
+ const api = await runningTenantApi(deps, tenant === undefined ? {} : { name: tenant });
31
+ const response = await deps.fetch(`${api.runtimeUrl}/v1/tenant/mcp/preview`, {
32
+ method: "POST",
23
33
  headers: {
24
34
  authorization: `Bearer ${api.managementKey}`,
25
35
  [PROTOCOL_HEADER]: String(PROTOCOL_VERSION),
26
36
  accept: "application/json",
27
- ...(body === undefined ? {} : { "content-type": "application/json" }),
37
+ "content-type": "application/json",
28
38
  },
29
- ...(body === undefined ? {} : { body: JSON.stringify(body) }),
39
+ body: JSON.stringify({
40
+ url,
41
+ ...(flags.booleans.has("--sse") ? { type: "sse" } : {}),
42
+ ...(name === undefined ? {} : { name }),
43
+ ...(vaultId === undefined ? {} : { vaultId }),
44
+ }),
30
45
  redirect: "error",
31
- signal: AbortSignal.timeout(150_000),
46
+ signal: AbortSignal.timeout(PREVIEW_TIMEOUT_MS),
32
47
  });
33
- const answer = (await response.json().catch(() => ({})));
34
- if (!response.ok)
35
- throw new CliError(`${method} ${path} returned ${response.status}${typeof answer.message === "string" ? `: ${answer.message}` : ""}`, 1);
36
- return answer;
37
- }
38
- /** The URL as the vault binds it: origin, path and query, no default port, no fragment. */
39
- function bindingUrl(input) {
40
- let url;
41
- try {
42
- url = new URL(input);
48
+ const body = (await response.json().catch(() => ({})));
49
+ if (response.status === 404 && body.code === "not_found")
50
+ throw new CliError(`Tenant ${api.name} runs a Runtime without MCP previews. Update it: npx nylorun@latest start.`, 1);
51
+ if (!response.ok) {
52
+ const details = body.details;
53
+ const failure = typeof details?.failure === "string" ? ` (${details.failure})` : "";
54
+ throw new CliError(typeof body.message === "string"
55
+ ? `${body.message}${failure}`
56
+ : `POST /v1/tenant/mcp/preview returned ${response.status}`, 1);
43
57
  }
44
- catch {
45
- throw usageError(`${input} is not a URL.`);
58
+ const preview = body;
59
+ if (flags.booleans.has("--json")) {
60
+ deps.out(JSON.stringify(preview, null, 2));
61
+ return 0;
46
62
  }
47
- if (url.protocol !== "https:" && url.protocol !== "http:")
48
- throw usageError(`${input} is not an http(s) URL.`);
49
- if (url.username || url.password || url.hash)
50
- throw usageError(`${input} must not carry userinfo or a fragment.`);
51
- return `${url.origin}${url.pathname}${url.search}`;
63
+ for (const line of previewLines(preview))
64
+ deps.out(line);
65
+ return 0;
52
66
  }
53
- /** The installation vault named `mcp`, created when there is none. */
54
- async function mcpVault(deps, api) {
55
- const { vaults } = await call(deps, api, "GET", "/v1/tenant/vaults");
56
- const found = vaults.find((vault) => vault.ownerUserId === "installation" && vault.name === MCP_VAULT_NAME);
57
- if (found)
58
- return found;
59
- const key = `nylorun-mcp-${randomUUID()}`;
60
- return await call(deps, api, "POST", "/v1/tenant/vaults", {
61
- requestId: key,
62
- idempotencyKey: key,
63
- name: MCP_VAULT_NAME,
64
- scope: "installation",
65
- });
67
+ /** A preview as a person reads it: the server, a table of tools, the renames, or the sign-in hint. */
68
+ export function previewLines(preview) {
69
+ const credential = preview.credentialSent
70
+ ? "with the installation vault's credential"
71
+ : "with no credential (no installation vault holds one for this URL)";
72
+ if (preview.authRequired) {
73
+ const metadata = preview.authRequired.resourceMetadata;
74
+ const servers = Array.isArray(metadata?.authorization_servers)
75
+ ? metadata.authorization_servers.filter((item) => typeof item === "string")
76
+ : [];
77
+ return [
78
+ preview.credentialSent
79
+ ? `The MCP server at ${preview.url} answered 401: it rejected the installation vault's credential.`
80
+ : `The MCP server at ${preview.url} answered 401: it needs a credential.`,
81
+ ...(servers.length > 0
82
+ ? [`It signs people in with ${servers.join(", ")} (${preview.authRequired.resourceMetadataUrl}).`]
83
+ : []),
84
+ "Nylorun holds no OAuth client: add the server's API key to an installation vault, or reach it",
85
+ "through a gateway that keeps each person's sign-in (a credential with via and an identity header).",
86
+ ];
87
+ }
88
+ const info = preview.serverInfo;
89
+ const label = info && typeof info.name === "string"
90
+ ? `${info.name}${typeof info.version === "string" ? ` ${info.version}` : ""}`
91
+ : preview.name;
92
+ const lines = [
93
+ `MCP server ${label} at ${preview.url}, ${credential}: ${preview.tools.length} ${preview.tools.length === 1 ? "tool" : "tools"}, named for the model as ${preview.name}__<tool>.`,
94
+ ];
95
+ if (preview.tools.length > 0) {
96
+ const rows = [
97
+ ["TOOL", "MODEL NAME", "SCHEMA", "HINTS", "DESCRIPTION"],
98
+ ...preview.tools.map((tool) => [
99
+ tool.serverToolName,
100
+ tool.modelName ?? "(left out: unusable input schema)",
101
+ `${tool.schemaBytes} B`,
102
+ hints(tool),
103
+ oneLine(tool.description ?? ""),
104
+ ]),
105
+ ];
106
+ const widths = rows[0].map((_, column) => Math.max(...rows.map((row) => row[column].length)));
107
+ lines.push("");
108
+ for (const row of rows)
109
+ lines.push(row
110
+ .map((cell, column) => (column === row.length - 1 ? cell : cell.padEnd(widths[column])))
111
+ .join(" ")
112
+ .trimEnd());
113
+ }
114
+ if (preview.renamed.length > 0) {
115
+ lines.push("", "Renamed for the model (characters outside A-Z, a-z, 0-9, _ and -, or past 64):");
116
+ for (const item of preview.renamed)
117
+ lines.push(` ${item.serverToolName} -> ${item.name}`);
118
+ }
119
+ return lines;
66
120
  }
67
- /** Each credential of the vault bound to `url`, by id, with when it last changed. */
68
- async function credentialsFor(deps, api, vaultId, url) {
69
- const { credentials } = await call(deps, api, "GET", `/v1/tenant/vaults/${encodeURIComponent(vaultId)}/credentials`);
70
- return new Map(credentials
71
- .filter((item) => item.type === "oauth" && item.binding.url === url)
72
- .map((item) => [item.id, item.rotatedAt ?? item.createdAt]));
121
+ function hints(tool) {
122
+ const annotations = tool.annotations ?? {};
123
+ const names = [
124
+ annotations.readOnlyHint === true ? "read-only" : undefined,
125
+ annotations.destructiveHint === true ? "destructive" : undefined,
126
+ annotations.idempotentHint === true ? "idempotent" : undefined,
127
+ ].filter((name) => name !== undefined);
128
+ return names.length === 0 ? "-" : names.join(",");
73
129
  }
74
- async function connect(deps, args) {
75
- const usage = "nylorun mcp connect <url> --server <name> [--vault <id>] [--client-id <id>] [--tenant <name>] [--no-open]";
76
- const flags = parseStackFlags(args, { booleans: ["--no-open"], values: ["--server", "--vault", "--client-id", "--tenant"] }, usage);
77
- if (flags.rest.length !== 1)
78
- throw usageError(`Usage: ${usage}`);
79
- const server = flags.values.get("--server");
80
- if (!server)
81
- throw usageError(`--server names the MCP server, as the agent declares it. Usage: ${usage}`);
82
- const url = bindingUrl(flags.rest[0]);
83
- const clientId = flags.values.get("--client-id");
84
- const name = flags.values.get("--tenant");
85
- const api = await runningTenantApi(deps, name === undefined ? {} : { name });
86
- const vaultId = flags.values.get("--vault") ?? (await mcpVault(deps, api)).id;
87
- const before = await credentialsFor(deps, api, vaultId, url);
88
- const started = await call(deps, api, "POST", `/v1/tenant/vaults/${encodeURIComponent(vaultId)}/oauth/start`, { url, server, ...(clientId === undefined ? {} : { clientId }) });
89
- deps.out(`Sign in to ${server} in your browser:`);
90
- deps.out(` ${started.authorizeUrl}`);
91
- if (!flags.booleans.has("--no-open"))
92
- await deps.openBrowser(started.authorizeUrl);
93
- deps.out("Waiting for the sign-in to finish…");
94
- const deadline = Math.min(Date.now() + CONNECT_TIMEOUT_MS, Date.parse(started.expiresAt) || Infinity);
95
- const pollMs = deps.pollMs ?? 1000;
96
- while (Date.now() < deadline) {
97
- await new Promise((resolve) => setTimeout(resolve, pollMs));
98
- const now = await credentialsFor(deps, api, vaultId, url);
99
- for (const [id, changed] of now)
100
- if (before.get(id) !== changed) {
101
- deps.out(`Connected ${server} (${url}): credential ${id} in installation vault ${vaultId}.`);
102
- deps.out(`Sessions use it when they attach the vault: vaultIds: ["${vaultId}"].`);
103
- return 0;
104
- }
105
- }
106
- throw new CliError(`The sign-in to ${server} did not finish in time. Run "nylorun mcp connect" again; the browser page says why when the server refused it.`, 1);
130
+ function oneLine(text) {
131
+ const flat = text.replace(/\s+/g, " ").trim();
132
+ return flat.length > DESCRIPTION_CHARS ? `${flat.slice(0, DESCRIPTION_CHARS - 1)}…` : flat;
107
133
  }
108
- /** `nylorun mcp <connect> ...` */
134
+ /** `nylorun mcp <inspect|connect> ...` */
109
135
  export async function mcpCommand(deps, args) {
110
136
  const [sub, ...rest] = args;
137
+ if (sub === "inspect")
138
+ return inspect(deps, rest);
111
139
  if (sub === "connect")
112
- return connect(deps, rest);
140
+ throw usageError(MCP_CONNECT_REMOVED);
113
141
  throw usageError(`Usage:\n${mcpUsage}`);
114
142
  }
@@ -19,7 +19,8 @@ export declare function listOperatorKeys(endpoint: OperateEndpoint): Promise<Ope
19
19
  export declare function deleteOperatorKey(endpoint: OperateEndpoint, id: string): Promise<boolean>;
20
20
  /**
21
21
  * Whether `key` reaches the Tenant at `runtimeUrl`: one authenticated read (`GET /v1/me`, which
22
- * every key reaches). An unknown key is the opaque 404; another answer is an error.
22
+ * every key reaches). An unknown key is `401 credential_invalid` (protocol 9; the opaque 404
23
+ * before), a key of no use here `403`; another answer is an error.
23
24
  */
24
25
  export declare function keyAuthenticates(fetch: FetchLike, runtimeUrl: string, key: string): Promise<boolean>;
25
26
  /**
@@ -43,7 +43,8 @@ export async function deleteOperatorKey(endpoint, id) {
43
43
  }
44
44
  /**
45
45
  * Whether `key` reaches the Tenant at `runtimeUrl`: one authenticated read (`GET /v1/me`, which
46
- * every key reaches). An unknown key is the opaque 404; another answer is an error.
46
+ * every key reaches). An unknown key is `401 credential_invalid` (protocol 9; the opaque 404
47
+ * before), a key of no use here `403`; another answer is an error.
47
48
  */
48
49
  export async function keyAuthenticates(fetch, runtimeUrl, key) {
49
50
  let response;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nylorun",
3
- "version": "0.11.1-beta",
3
+ "version": "0.12.0-beta",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0",
6
6
  "description": "Set up and run local Nylorun Tenants (Runtime and Studio) with Docker Compose",
@@ -30,7 +30,7 @@
30
30
  "prepack": "npm run build"
31
31
  },
32
32
  "dependencies": {
33
- "@nylorun/core": "0.15.0-beta"
33
+ "@nylorun/core": "0.17.0-beta"
34
34
  },
35
35
  "devDependencies": {
36
36
  "typescript": "npm:@typescript/typescript6@^6.0.2",
@@ -41,7 +41,7 @@
41
41
  "nylorun": "dist/cli.js"
42
42
  },
43
43
  "nylorun": {
44
- "runtime": "0.19.1-beta",
45
- "studio": "0.22.1-beta"
44
+ "runtime": "0.21.0-beta",
45
+ "studio": "0.23.0-beta"
46
46
  }
47
47
  }