nylorun 0.11.2-beta → 0.12.1-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,63 @@
1
1
  # nylorun
2
2
 
3
+ ## 0.12.1-beta
4
+
5
+ ### Patch Changes
6
+
7
+ - 7780b2f: **One client and connection layer.** The Project link and credentials are read one way, and every Runtime client shares one `/health` probe.
8
+
9
+ - `@nylorun/core`: `@nylorun/core/project` (Node only, the one Core subpath that imports Node modules) reads the Nylorun home, a local Tenant's Host root (`tenantHostRoot`), the Project root (`findProjectRoot`) and the Project link and credentials (`findLinkedProject`, `readProjectLink`, `readCredentialsFile`), validated with `ProjectLinkFileSchema` and `ProjectCredentialsFileSchema`; a broken or newer file throws `ProjectFileError`. `@nylorun/core/transport` (browser-safe) holds the `/health` compatibility probe (`checkHealth`), `parseProtocolRange` (on `ProtocolRangeSchema`, ignoring fields a newer Host adds), `describeIncompatibility`, `requestHeaders` and `readBody`.
10
+ - `@nylorun/agents`: `resolveConnection` and `createClient()` find the Project as every reader does: the nearest directory with `.nylorun/` from `cwd` upwards, never the home directory or above it (they used to walk to the filesystem root, and past a `.nylorun/` without a link). A link without `credentials.json`, or a link or credentials file that does not validate, is now `connection_missing` naming the file and `npx nylorun start`, instead of a raw file-system or parse error. `@nylorun/agents/client` loads the link reader only when `createClient()` resolves a connection, so it imports no Node module. The root entry re-exports `checkHealth`, `describeIncompatibility` and `parseProtocolRange`.
11
+ - `@nylorun/admin`: `createAdmin` reads the Project link with the same reader. A link that does not validate is now refused like one from an older nylorun (when nothing else names the Host root), and a linked Project's `credentials.json` that does not validate is `connection_missing` instead of being skipped. `@nylorun/admin/project` re-exports `@nylorun/core/project` for tools that depend on this package. The `/health` check and request headers come from `@nylorun/core/transport`.
12
+ - `nylorun`: reads the Project link and credentials with `@nylorun/core/project`; a credentials file whose keys are not 64 hex characters is replaced by `start`, as an unreadable one was. No command changes.
13
+ - `@nylorun/cli`: `nylo` reads the Project root, link and credentials through `@nylorun/admin/project` instead of its own copies, with the same messages. No command changes.
14
+ - `@nylorun/studio`: the server's Runtime compatibility probe is `checkHealth` from `@nylorun/agents`.
15
+
16
+ - Pin core to the tested release.
17
+ - Pin runtime to the tested release.
18
+ - Pin studio to the tested release.
19
+ - Updated dependencies [7780b2f]
20
+ - Updated dependencies [070674d]
21
+ - @nylorun/core@0.18.0-beta
22
+
23
+ ## 0.12.0-beta
24
+
25
+ ### Major Changes
26
+
27
+ - 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.
28
+
29
+ - **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.
30
+ - **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`.
31
+ - **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.
32
+ - **Breaking (`@nylorun/admin`):** `admin.vaults` loses its OAuth start method.
33
+ - **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.
34
+ - `@nylorun/studio`: the Credentials page loses the OAuth type, the Expires column and the OAuth connect hint.
35
+
36
+ ### Minor Changes
37
+
38
+ - da93711: **Preview an MCP server's tools** (R2b C12). MIGRATION.md (protocol 10, "Previewing a server's tools") has the details.
39
+
40
+ - `@nylorun/core`: `McpPreviewRequestSchema` and `McpPreviewSchema` (`McpPreviewTool`), and the error code `mcp_preview_failed`.
41
+ - `@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.
42
+ - `@nylorun/admin`: `admin.mcp.preview({ url, type?, name?, vaultId? })`.
43
+ - `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`.
44
+ - `@nylorun/studio`: **Preview tools** on each credential of the Credentials page lists the tools behind its URL.
45
+
46
+ ### Patch Changes
47
+
48
+ - 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.
49
+ - Pin core to the tested release.
50
+ - Pin runtime to the tested release.
51
+ - Pin studio to the tested release.
52
+ - Updated dependencies [50e2f7e]
53
+ - Updated dependencies [e0e39ff]
54
+ - Updated dependencies [fed5e58]
55
+ - Updated dependencies [18f9a2f]
56
+ - Updated dependencies [da93711]
57
+ - Updated dependencies [90a817d]
58
+ - Updated dependencies [b8d10cb]
59
+ - @nylorun/core@0.17.0-beta
60
+
3
61
  ## 0.11.2-beta
4
62
 
5
63
  ### 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;
@@ -1,19 +1,12 @@
1
+ import { projectCredentialsPath, projectLinkPath, type ProjectLink } from "@nylorun/core/project";
1
2
  /**
2
3
  * The Project link (`.nylorun/link.json`) and credentials (`.nylorun/credentials.json`).
3
4
  * `nylorun start` writes both, format 3: the local Tenant's name, its URL and Host id, and
4
5
  * the Tenant id as information (nothing in a request selects a Tenant). `start` replaces a link
5
- * of an older format.
6
+ * of an older format. Reading is `@nylorun/core/project`'s, which every client shares.
6
7
  */
7
- export interface ProjectLink {
8
- format: 0 | 1 | 2 | 3;
9
- /** The local Tenant's name (format 3). */
10
- tenant?: string;
11
- hostUrl: string;
12
- hostId: string;
13
- tenantId?: string;
14
- }
15
- export declare function linkPath(projectRoot: string): string;
16
- export declare function credentialsPath(projectRoot: string): string;
8
+ export type { ProjectLink };
9
+ export { projectLinkPath as linkPath, projectCredentialsPath as credentialsPath };
17
10
  /** The Project's link, or undefined when it has none. An unreadable link is an error. */
18
11
  export declare function readProjectLink(projectRoot: string): Promise<ProjectLink | undefined>;
19
12
  export declare function writeProjectLink(projectRoot: string, link: {
@@ -37,9 +30,8 @@ export interface KeyCredentials {
37
30
  /** The Project's keys, or undefined when absent or unreadable. */
38
31
  export declare function readProjectCredentials(projectRoot: string): Promise<KeyCredentials | undefined>;
39
32
  /**
40
- * A credentials file (`{ format: 1, applicationKey, principalId, managementKey?,
41
- * managementPrincipalId? }`): the Project's, or one the nylorun commands keep in the Host root.
42
- * Undefined when absent or unreadable.
33
+ * A credentials file (format 0 or 1): the Project's, or one the nylorun commands keep in the
34
+ * Host root. Undefined when absent or unreadable, so `start` replaces it.
43
35
  */
44
36
  export declare function readCredentialsFile(path: string): Promise<KeyCredentials | undefined>;
45
37
  /** `.nylorun/credentials.json` (format 1, mode 0600). */
@@ -1,43 +1,19 @@
1
1
  import { randomBytes } from "node:crypto";
2
- import { chmod, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
2
+ import { chmod, mkdir, rename, rm, writeFile } from "node:fs/promises";
3
3
  import { join } from "node:path";
4
- import { ProjectLinkFileSchema } from "@nylorun/core/contracts";
4
+ import { ProjectFileError, projectCredentialsPath, projectLinkPath, readCredentialsFile as readCredentials, readProjectLink as readLink, } from "@nylorun/core/project";
5
5
  import { CliError } from "../errors.js";
6
- export function linkPath(projectRoot) {
7
- return join(projectRoot, ".nylorun", "link.json");
8
- }
9
- export function credentialsPath(projectRoot) {
10
- return join(projectRoot, ".nylorun", "credentials.json");
11
- }
6
+ export { projectLinkPath as linkPath, projectCredentialsPath as credentialsPath };
12
7
  /** The Project's link, or undefined when it has none. An unreadable link is an error. */
13
8
  export async function readProjectLink(projectRoot) {
14
- let text;
15
9
  try {
16
- text = await readFile(linkPath(projectRoot), "utf8");
10
+ return readLink(projectRoot);
17
11
  }
18
12
  catch (error) {
19
- if (error.code === "ENOENT")
20
- return undefined;
13
+ if (error instanceof ProjectFileError)
14
+ throw new CliError(`Invalid or newer Project link at ${error.path}. Upgrade nylorun, or remove the file and run "npx nylorun start" again.`, 1);
21
15
  throw error;
22
16
  }
23
- let value;
24
- try {
25
- value = JSON.parse(text);
26
- }
27
- catch {
28
- value = undefined;
29
- }
30
- const parsed = ProjectLinkFileSchema.safeParse(value);
31
- if (!parsed.success)
32
- throw new CliError(`Invalid or newer Project link at ${linkPath(projectRoot)}. Upgrade nylorun, or remove the file and run "npx nylorun start" again.`, 1);
33
- const link = parsed.data;
34
- return {
35
- format: link.format,
36
- ...(link.format === 3 && link.tenant ? { tenant: link.tenant } : {}),
37
- hostUrl: link.hostUrl.replace(/\/$/, ""),
38
- hostId: link.hostId,
39
- ...(link.tenantId ? { tenantId: link.tenantId } : {}),
40
- };
41
17
  }
42
18
  async function writePrivate(path, value) {
43
19
  const temporary = `${path}.${randomBytes(8).toString("hex")}.tmp`;
@@ -59,7 +35,7 @@ async function ensureProjectDir(projectRoot) {
59
35
  }
60
36
  export async function writeProjectLink(projectRoot, link) {
61
37
  await ensureProjectDir(projectRoot);
62
- await writePrivate(linkPath(projectRoot), {
38
+ await writePrivate(projectLinkPath(projectRoot), {
63
39
  format: 3,
64
40
  tenant: link.tenant,
65
41
  hostUrl: link.hostUrl.replace(/\/$/, ""),
@@ -69,34 +45,34 @@ export async function writeProjectLink(projectRoot, link) {
69
45
  }
70
46
  /** The Project's keys, or undefined when absent or unreadable. */
71
47
  export async function readProjectCredentials(projectRoot) {
72
- return await readCredentialsFile(credentialsPath(projectRoot));
48
+ return await readCredentialsFile(projectCredentialsPath(projectRoot));
73
49
  }
74
50
  /**
75
- * A credentials file (`{ format: 1, applicationKey, principalId, managementKey?,
76
- * managementPrincipalId? }`): the Project's, or one the nylorun commands keep in the Host root.
77
- * Undefined when absent or unreadable.
51
+ * A credentials file (format 0 or 1): the Project's, or one the nylorun commands keep in the
52
+ * Host root. Undefined when absent or unreadable, so `start` replaces it.
78
53
  */
79
54
  export async function readCredentialsFile(path) {
55
+ let value;
80
56
  try {
81
- const value = JSON.parse(await readFile(path, "utf8"));
82
- if (typeof value.applicationKey !== "string" || typeof value.principalId !== "string")
83
- return undefined;
84
- return {
85
- applicationKey: value.applicationKey,
86
- principalId: value.principalId,
87
- ...(typeof value.managementKey === "string" && typeof value.managementPrincipalId === "string"
88
- ? { management: { key: value.managementKey, principalId: value.managementPrincipalId } }
89
- : {}),
90
- };
57
+ value = readCredentials(path);
91
58
  }
92
59
  catch {
93
60
  return undefined;
94
61
  }
62
+ if (!value)
63
+ return undefined;
64
+ return {
65
+ applicationKey: value.applicationKey,
66
+ principalId: value.principalId,
67
+ ...(value.managementKey && value.managementPrincipalId
68
+ ? { management: { key: value.managementKey, principalId: value.managementPrincipalId } }
69
+ : {}),
70
+ };
95
71
  }
96
72
  /** `.nylorun/credentials.json` (format 1, mode 0600). */
97
73
  export async function writeProjectCredentials(projectRoot, credentials) {
98
74
  await ensureProjectDir(projectRoot);
99
- await writeCredentialsFile(credentialsPath(projectRoot), credentials);
75
+ await writeCredentialsFile(projectCredentialsPath(projectRoot), credentials);
100
76
  }
101
77
  /** A credentials file (format 1), written atomically with mode 0600. */
102
78
  export async function writeCredentialsFile(path, credentials) {
@@ -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;
@@ -2,9 +2,9 @@ import { existsSync } from "node:fs";
2
2
  import { mkdir, readFile, rm } from "node:fs/promises";
3
3
  import { basename, resolve } from "node:path";
4
4
  import { compareVersions } from "@nylorun/core/compatibility";
5
+ import { findProjectRoot } from "@nylorun/core/project";
5
6
  import { CliError } from "../errors.js";
6
7
  import { credentialsPath, readCredentialsFile, readProjectCredentials, readProjectLink, writeProjectCredentials, writeProjectLink, } from "../project/link.js";
7
- import { findProjectRoot } from "../project/root.js";
8
8
  import { seedTenant } from "../project/seed.js";
9
9
  import { readTelemetry, STUDIO_ANALYTICS_ID, TELEMETRY_NOTICE, telemetryDecision, writeTelemetry, } from "../telemetry.js";
10
10
  import { dockerPreflight, formatBytes, parseComposePs, tenantMemory, } from "./docker.js";
@@ -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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nylorun",
3
- "version": "0.11.2-beta",
3
+ "version": "0.12.1-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.16.0-beta"
33
+ "@nylorun/core": "0.18.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.20.0-beta",
45
- "studio": "0.22.2-beta"
44
+ "runtime": "0.21.1-beta",
45
+ "studio": "0.23.1-beta"
46
46
  }
47
47
  }
package/dist/home.d.ts DELETED
@@ -1,6 +0,0 @@
1
- /**
2
- * The Host root: an explicit home, `NYLORUN_HOME`, or `~/.nylorun`. Compose
3
- * bind-mounts it into the Runtime and Studio containers, and `@nylorun/admin`
4
- * reads `host.json` and the admin key from it.
5
- */
6
- export declare function resolveHome(home?: string, env?: Readonly<Record<string, string | undefined>>): string;
package/dist/home.js DELETED
@@ -1,15 +0,0 @@
1
- import { homedir } from "node:os";
2
- import { join, resolve } from "node:path";
3
- /**
4
- * The Host root: an explicit home, `NYLORUN_HOME`, or `~/.nylorun`. Compose
5
- * bind-mounts it into the Runtime and Studio containers, and `@nylorun/admin`
6
- * reads `host.json` and the admin key from it.
7
- */
8
- export function resolveHome(home, env = process.env) {
9
- if (home !== undefined && home.trim() !== "")
10
- return resolve(home);
11
- const fromEnv = env.NYLORUN_HOME?.trim();
12
- if (fromEnv)
13
- return resolve(fromEnv);
14
- return resolve(join(homedir(), ".nylorun"));
15
- }
@@ -1,7 +0,0 @@
1
- /**
2
- * The nearest `.nylorun` wins so an initialised Project keeps working from a
3
- * subdirectory; otherwise the nearest package.json defines the Project. The
4
- * walk stops at the home directory because `~/.nylorun` is the Host root, never
5
- * a Project root.
6
- */
7
- export declare function findProjectRoot(cwd?: string): string | undefined;
@@ -1,44 +0,0 @@
1
- import { existsSync, realpathSync } from "node:fs";
2
- import { homedir } from "node:os";
3
- import { dirname, join, parse, resolve } from "node:path";
4
- /**
5
- * The nearest `.nylorun` wins so an initialised Project keeps working from a
6
- * subdirectory; otherwise the nearest package.json defines the Project. The
7
- * walk stops at the home directory because `~/.nylorun` is the Host root, never
8
- * a Project root.
9
- */
10
- export function findProjectRoot(cwd = process.cwd()) {
11
- const stop = (() => {
12
- try {
13
- return realpathSync(homedir());
14
- }
15
- catch {
16
- return homedir();
17
- }
18
- })();
19
- let directory = resolve(cwd);
20
- try {
21
- directory = realpathSync(directory);
22
- }
23
- catch {
24
- /* unresolvable cwd falls through */
25
- }
26
- const root = parse(directory).root;
27
- let fallback;
28
- for (;;) {
29
- // The home directory is never a Project, but a package.json below it is.
30
- if (directory === stop)
31
- return fallback;
32
- if (existsSync(join(directory, ".nylorun")))
33
- return directory;
34
- if (!fallback && existsSync(join(directory, "package.json")))
35
- fallback = directory;
36
- if (directory === root)
37
- break;
38
- const parent = dirname(directory);
39
- if (parent === directory)
40
- break;
41
- directory = parent;
42
- }
43
- return fallback;
44
- }