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 +50 -0
- package/README.md +29 -11
- package/dist/cli.js +4 -4
- package/dist/stack/commands.d.ts +1 -1
- package/dist/stack/compose-file.d.ts +1 -1
- package/dist/stack/compose-file.js +4 -8
- package/dist/stack/index.d.ts +3 -3
- package/dist/stack/index.js +6 -6
- package/dist/stack/mcp.d.ts +7 -6
- package/dist/stack/mcp.js +118 -90
- package/dist/stack/operator-keys.d.ts +2 -1
- package/dist/stack/operator-keys.js +2 -1
- package/package.json +4 -4
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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;
|
package/dist/stack/commands.d.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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:
|
package/dist/stack/index.d.ts
CHANGED
|
@@ -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;
|
package/dist/stack/index.js
CHANGED
|
@@ -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);
|
package/dist/stack/mcp.d.ts
CHANGED
|
@@ -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
|
|
3
|
-
/**
|
|
4
|
-
export declare const
|
|
5
|
-
/**
|
|
6
|
-
export declare
|
|
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
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
export const
|
|
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
|
|
21
|
-
const
|
|
22
|
-
|
|
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
|
-
|
|
37
|
+
"content-type": "application/json",
|
|
28
38
|
},
|
|
29
|
-
|
|
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(
|
|
46
|
+
signal: AbortSignal.timeout(PREVIEW_TIMEOUT_MS),
|
|
32
47
|
});
|
|
33
|
-
const
|
|
34
|
-
if (
|
|
35
|
-
throw new CliError(
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
45
|
-
|
|
58
|
+
const preview = body;
|
|
59
|
+
if (flags.booleans.has("--json")) {
|
|
60
|
+
deps.out(JSON.stringify(preview, null, 2));
|
|
61
|
+
return 0;
|
|
46
62
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
/**
|
|
54
|
-
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
.
|
|
72
|
-
.
|
|
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
|
-
|
|
75
|
-
const
|
|
76
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
45
|
-
"studio": "0.
|
|
44
|
+
"runtime": "0.21.0-beta",
|
|
45
|
+
"studio": "0.23.0-beta"
|
|
46
46
|
}
|
|
47
47
|
}
|