@nylorun/runtime 0.6.0-beta → 0.8.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.
Files changed (85) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/README.md +9 -5
  3. package/dist/adapters/sandbox/microsandbox.d.ts +2 -0
  4. package/dist/adapters/sandbox/microsandbox.js +238 -0
  5. package/dist/adapters/sandbox/virtual.d.ts +4 -0
  6. package/dist/adapters/sandbox/virtual.js +125 -0
  7. package/dist/configuration.d.ts +1 -1
  8. package/dist/configuration.js +1 -1
  9. package/dist/core/executors.d.ts +32 -0
  10. package/dist/core/executors.js +60 -0
  11. package/dist/core/main.js +7 -12
  12. package/dist/core/runtime.d.ts +41 -1
  13. package/dist/core/runtime.js +594 -53
  14. package/dist/core/store.d.ts +13 -0
  15. package/dist/core/store.js +95 -1
  16. package/dist/index.d.ts +0 -1
  17. package/dist/index.js +0 -1
  18. package/dist/mcp/connect.d.ts +31 -0
  19. package/dist/mcp/connect.js +190 -0
  20. package/dist/mcp/pool.d.ts +45 -0
  21. package/dist/mcp/pool.js +143 -0
  22. package/dist/mcp/snapshot.d.ts +25 -0
  23. package/dist/mcp/snapshot.js +10 -0
  24. package/dist/mcp/stdio.d.ts +15 -0
  25. package/dist/mcp/stdio.js +80 -0
  26. package/dist/model/catalog.d.ts +12 -0
  27. package/dist/model/catalog.js +29 -0
  28. package/dist/model/models.d.ts +3 -1
  29. package/dist/model/models.js +4 -2
  30. package/dist/model/pi-model.d.ts +9 -3
  31. package/dist/model/pi-model.js +47 -11
  32. package/dist/model/settings.d.ts +0 -2
  33. package/dist/model/settings.js +0 -47
  34. package/dist/node/index.d.ts +1 -1
  35. package/dist/node/index.js +1 -1
  36. package/dist/sandbox/manager.d.ts +61 -0
  37. package/dist/sandbox/manager.js +239 -0
  38. package/dist/sandbox/policy.d.ts +13 -0
  39. package/dist/sandbox/policy.js +67 -0
  40. package/dist/sandbox/select.d.ts +35 -0
  41. package/dist/sandbox/select.js +76 -0
  42. package/dist/sandbox/tools.d.ts +25 -0
  43. package/dist/sandbox/tools.js +165 -0
  44. package/dist/sandbox/types.d.ts +66 -0
  45. package/dist/vault/crypto.d.ts +12 -0
  46. package/dist/vault/crypto.js +80 -0
  47. package/dist/vault/error.d.ts +4 -0
  48. package/dist/vault/error.js +7 -0
  49. package/dist/vault/kek.d.ts +6 -0
  50. package/dist/vault/kek.js +26 -0
  51. package/dist/vault/service.d.ts +99 -0
  52. package/dist/vault/service.js +825 -0
  53. package/dist/vault/url.d.ts +3 -0
  54. package/dist/vault/url.js +27 -0
  55. package/dist/version.d.ts +1 -0
  56. package/dist/version.js +3 -0
  57. package/package.json +12 -6
  58. package/dist/config.d.ts +0 -20
  59. package/dist/model/defaults.d.ts +0 -17
  60. package/dist/model/defaults.js +0 -21
  61. package/dist/model/http-model.d.ts +0 -12
  62. package/dist/model/http-model.js +0 -299
  63. package/dist/node/local-sessions.d.ts +0 -5
  64. package/dist/node/local-sessions.js +0 -174
  65. package/dist/server/ag-ui.d.ts +0 -8
  66. package/dist/server/ag-ui.js +0 -76
  67. package/dist/server/delivery.d.ts +0 -24
  68. package/dist/server/delivery.js +0 -107
  69. package/dist/server/digests.d.ts +0 -13
  70. package/dist/server/digests.js +0 -39
  71. package/dist/server/host.d.ts +0 -70
  72. package/dist/server/host.js +0 -647
  73. package/dist/session/api.d.ts +0 -10
  74. package/dist/session/api.js +0 -15
  75. package/dist/session/default.d.ts +0 -5
  76. package/dist/session/default.js +0 -30
  77. package/dist/session/handle.d.ts +0 -27
  78. package/dist/session/handle.js +0 -199
  79. package/dist/session/index.d.ts +0 -2
  80. package/dist/session/index.js +0 -2
  81. package/dist/sessions/host.d.ts +0 -39
  82. package/dist/sessions/host.js +0 -359
  83. package/dist/sessions/store.d.ts +0 -41
  84. package/dist/sessions/store.js +0 -33
  85. /package/dist/{config.js → sandbox/types.js} +0 -0
@@ -1,6 +1,14 @@
1
1
  import { DatabaseSync } from "node:sqlite";
2
2
  import type { LiveEvent } from "@nylorun/core/contracts";
3
3
  export declare function canonical(value: unknown): string;
4
+ export interface ExecutorRow {
5
+ agentId: string;
6
+ tokenHash: string;
7
+ implementationVersion: string;
8
+ manifestHash?: string;
9
+ createdAt: string;
10
+ updatedAt: string;
11
+ }
4
12
  export declare class Store {
5
13
  readonly db: DatabaseSync;
6
14
  constructor(path: string);
@@ -8,7 +16,12 @@ export declare class Store {
8
16
  get<T = any>(table: string, id: string): T | undefined;
9
17
  all<T = any>(table: string): T[];
10
18
  put(table: string, id: string, body: unknown): void;
19
+ delete(table: string, id: string): void;
20
+ allExecutors(): ExecutorRow[];
21
+ putExecutor(row: Omit<ExecutorRow, "createdAt">): void;
22
+ deleteExecutor(agentId: string): void;
11
23
  event(sessionId: string, turnId: string | null, type: string, payload: unknown): LiveEvent;
24
+ credentialCount(): number;
12
25
  history(sessionId: string, cursor?: string): {
13
26
  items: LiveEvent[];
14
27
  cursor: string | null;
@@ -25,8 +25,58 @@ export class Store {
25
25
  CREATE TABLE IF NOT EXISTS checkpoints(id TEXT PRIMARY KEY, body TEXT NOT NULL);
26
26
  CREATE TABLE IF NOT EXISTS effects(id TEXT PRIMARY KEY, body TEXT NOT NULL);
27
27
  CREATE TABLE IF NOT EXISTS actions(id TEXT PRIMARY KEY, body TEXT NOT NULL);
28
+ CREATE TABLE IF NOT EXISTS sandboxes(id TEXT PRIMARY KEY, body TEXT NOT NULL);
28
29
  CREATE TABLE IF NOT EXISTS events(sequence INTEGER PRIMARY KEY AUTOINCREMENT, session_id TEXT NOT NULL, body TEXT NOT NULL);
29
- CREATE INDEX IF NOT EXISTS events_session ON events(session_id, sequence);`);
30
+ CREATE INDEX IF NOT EXISTS events_session ON events(session_id, sequence);
31
+ CREATE TABLE IF NOT EXISTS vaults(
32
+ id TEXT PRIMARY KEY,
33
+ name TEXT NOT NULL,
34
+ owner_user_id TEXT NOT NULL,
35
+ metadata_json TEXT,
36
+ created_at TEXT NOT NULL,
37
+ scope TEXT NOT NULL DEFAULT 'user'
38
+ );
39
+ CREATE INDEX IF NOT EXISTS vaults_owner ON vaults(owner_user_id);
40
+ CREATE TABLE IF NOT EXISTS vault_credentials(
41
+ id TEXT PRIMARY KEY,
42
+ vault_id TEXT NOT NULL REFERENCES vaults(id) ON DELETE CASCADE,
43
+ name TEXT NOT NULL,
44
+ type TEXT NOT NULL,
45
+ binding_json TEXT NOT NULL,
46
+ expires_at TEXT,
47
+ created_at TEXT NOT NULL,
48
+ rotated_at TEXT,
49
+ kek_id TEXT NOT NULL,
50
+ nonce BLOB NOT NULL,
51
+ ciphertext BLOB NOT NULL,
52
+ wrapped_dek BLOB NOT NULL
53
+ );
54
+ CREATE INDEX IF NOT EXISTS vault_credentials_vault ON vault_credentials(vault_id);
55
+ CREATE TABLE IF NOT EXISTS vault_audit(
56
+ id TEXT PRIMARY KEY,
57
+ at TEXT NOT NULL,
58
+ actor TEXT NOT NULL,
59
+ action TEXT NOT NULL,
60
+ vault_id TEXT,
61
+ credential_id TEXT,
62
+ session_id TEXT,
63
+ target TEXT,
64
+ outcome TEXT NOT NULL
65
+ );
66
+ CREATE TABLE IF NOT EXISTS vault_idempotency(
67
+ id TEXT PRIMARY KEY,
68
+ body_hash TEXT NOT NULL,
69
+ response TEXT NOT NULL
70
+ );
71
+ CREATE TABLE IF NOT EXISTS executors(
72
+ agent_id TEXT PRIMARY KEY,
73
+ token_hash TEXT NOT NULL UNIQUE,
74
+ implementation_version TEXT NOT NULL,
75
+ manifest_hash TEXT,
76
+ created_at TEXT NOT NULL,
77
+ updated_at TEXT NOT NULL
78
+ );`);
79
+ migrateVaultScope(this.db);
30
80
  }
31
81
  tx(fn) {
32
82
  this.db.exec("BEGIN IMMEDIATE");
@@ -55,6 +105,38 @@ export class Store {
55
105
  .prepare(`INSERT INTO ${table}(id,body) VALUES(?,?) ON CONFLICT(id) DO UPDATE SET body=excluded.body`)
56
106
  .run(id, JSON.stringify(body));
57
107
  }
108
+ delete(table, id) {
109
+ this.db.prepare(`DELETE FROM ${table} WHERE id=?`).run(id);
110
+ }
111
+ // Executors carry a bearer secret, so they use typed columns with a database-enforced unique
112
+ // token hash instead of flowing through the generic JSON document helpers above.
113
+ allExecutors() {
114
+ return this.db
115
+ .prepare("SELECT agent_id,token_hash,implementation_version,manifest_hash,created_at,updated_at FROM executors")
116
+ .all()
117
+ .map((r) => ({
118
+ agentId: String(r.agent_id),
119
+ tokenHash: String(r.token_hash),
120
+ implementationVersion: String(r.implementation_version),
121
+ manifestHash: r.manifest_hash == null ? undefined : String(r.manifest_hash),
122
+ createdAt: String(r.created_at),
123
+ updatedAt: String(r.updated_at),
124
+ }));
125
+ }
126
+ putExecutor(row) {
127
+ this.db
128
+ .prepare(`INSERT INTO executors(agent_id,token_hash,implementation_version,manifest_hash,created_at,updated_at)
129
+ VALUES(?,?,?,?,?,?)
130
+ ON CONFLICT(agent_id) DO UPDATE SET
131
+ token_hash=excluded.token_hash,
132
+ implementation_version=excluded.implementation_version,
133
+ manifest_hash=excluded.manifest_hash,
134
+ updated_at=excluded.updated_at`)
135
+ .run(row.agentId, row.tokenHash, row.implementationVersion, row.manifestHash ?? null, row.updatedAt, row.updatedAt);
136
+ }
137
+ deleteExecutor(agentId) {
138
+ this.db.prepare("DELETE FROM executors WHERE agent_id=?").run(agentId);
139
+ }
58
140
  event(sessionId, turnId, type, payload) {
59
141
  const base = {
60
142
  eventId: randomUUID(),
@@ -76,6 +158,12 @@ export class Store {
76
158
  .run(JSON.stringify(event), result.lastInsertRowid);
77
159
  return event;
78
160
  }
161
+ credentialCount() {
162
+ const row = this.db
163
+ .prepare("SELECT COUNT(*) AS n FROM vault_credentials")
164
+ .get();
165
+ return Number(row.n);
166
+ }
79
167
  history(sessionId, cursor) {
80
168
  let sequence = 0;
81
169
  if (cursor) {
@@ -99,3 +187,9 @@ export class Store {
99
187
  };
100
188
  }
101
189
  }
190
+ function migrateVaultScope(db) {
191
+ const columns = db.prepare("PRAGMA table_info(vaults)").all();
192
+ if (!columns.some((column) => column.name === "scope"))
193
+ db.exec("ALTER TABLE vaults ADD COLUMN scope TEXT NOT NULL DEFAULT 'user'");
194
+ db.exec("CREATE UNIQUE INDEX IF NOT EXISTS vaults_one_host ON vaults(scope) WHERE scope = 'host'");
195
+ }
package/dist/index.d.ts CHANGED
@@ -1,6 +1,5 @@
1
1
  export { CoreRuntime, createRuntime, startRuntime, type RuntimeOptions } from "./core/runtime.js";
2
2
  export { scriptedModel, gatewayModel, type ModelProvider } from "./core/provider.js";
3
- export { httpModel, type HttpModelOptions, type ModelEnvironment } from "./model/http-model.js";
4
3
  export type { RuntimeAgent } from "./contracts.js";
5
4
  export { IMAGE_MEDIA_TYPES, MAX_IMAGE_BYTES, decodeImageBase64, validateImageBytes } from "./media.js";
6
5
  export type { RuntimeModelAdapter } from "./contracts.js";
package/dist/index.js CHANGED
@@ -1,4 +1,3 @@
1
1
  export { CoreRuntime, createRuntime, startRuntime } from "./core/runtime.js";
2
2
  export { scriptedModel, gatewayModel } from "./core/provider.js";
3
- export { httpModel } from "./model/http-model.js";
4
3
  export { IMAGE_MEDIA_TYPES, MAX_IMAGE_BYTES, decodeImageBase64, validateImageBytes } from "./media.js";
@@ -0,0 +1,31 @@
1
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
2
+ import type { McpServerManifest } from "@nylorun/core/define";
3
+ import type { AuthorizeResult } from "../vault/service.js";
4
+ import { type McpDiagnostic, type McpToolRecord } from "./snapshot.js";
5
+ export interface LiveConnection {
6
+ readonly client: Client;
7
+ close(): Promise<void>;
8
+ }
9
+ export declare function openMcpServer(input: {
10
+ server: McpServerManifest;
11
+ authorize?: (url: string) => Promise<AuthorizeResult>;
12
+ pluginRoot?: string;
13
+ pluginData: string;
14
+ }): Promise<LiveConnection>;
15
+ export declare function listMcpTools(client: Client, input: {
16
+ capabilityId: string;
17
+ serverName: string;
18
+ taken: Set<string>;
19
+ }): Promise<{
20
+ tools: McpToolRecord[];
21
+ omitted: string[];
22
+ }>;
23
+ export declare function callMcpTool(client: Client, serverToolName: string, args: unknown): Promise<{
24
+ kind: "completed";
25
+ output: unknown;
26
+ } | {
27
+ kind: "failed";
28
+ code: string;
29
+ message: string;
30
+ }>;
31
+ export declare function diagnosticFromError(capabilityId: string, serverName: string, error: unknown): McpDiagnostic;
@@ -0,0 +1,190 @@
1
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
2
+ import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
3
+ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
4
+ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
5
+ import { schemaFromJSON } from "@nylorun/core/define";
6
+ import { modelToolName, } from "./snapshot.js";
7
+ import { prepareStdioLaunch } from "./stdio.js";
8
+ export async function openMcpServer(input) {
9
+ if (input.server.type === "stdio") {
10
+ const launch = prepareStdioLaunch(input.server, {
11
+ pluginRoot: input.pluginRoot,
12
+ pluginData: input.pluginData,
13
+ });
14
+ const transport = new StdioClientTransport({
15
+ command: launch.command,
16
+ args: [...launch.args],
17
+ cwd: launch.cwd,
18
+ env: { ...launch.env },
19
+ stderr: "pipe",
20
+ });
21
+ transport.stderr?.on("data", () => { });
22
+ const client = createClient();
23
+ await client.connect(transport);
24
+ return { client, close: () => client.close() };
25
+ }
26
+ const authorize = input.authorize;
27
+ if (!authorize)
28
+ throw new Error("HTTP MCP server requires authorization");
29
+ const initial = await authorize(input.server.url);
30
+ if (initial.status === "refused") {
31
+ const error = new Error(`MCP authorization refused: ${initial.reason}`);
32
+ error.credentialIds =
33
+ initial.credentialIds;
34
+ throw error;
35
+ }
36
+ const fetchImpl = authorizedFetch(input.server, authorize);
37
+ const url = new URL(input.server.url);
38
+ const transport = input.server.type === "sse"
39
+ ? new SSEClientTransport(url, { fetch: fetchImpl })
40
+ : new StreamableHTTPClientTransport(url, { fetch: fetchImpl });
41
+ const client = createClient();
42
+ await client.connect(transport);
43
+ return { client, close: () => client.close() };
44
+ }
45
+ export async function listMcpTools(client, input) {
46
+ const tools = [];
47
+ const omitted = [];
48
+ let cursor;
49
+ for (let page = 0; page < 100; page += 1) {
50
+ const listed = await client.listTools(cursor ? { cursor } : undefined);
51
+ for (const tool of listed.tools) {
52
+ if (typeof tool.name !== "string" || tool.name.length === 0)
53
+ continue;
54
+ const name = modelToolName(input.serverName, tool.name);
55
+ const inputSchema = usableSchema(tool.inputSchema);
56
+ if (!inputSchema || input.taken.has(name)) {
57
+ omitted.push(name);
58
+ continue;
59
+ }
60
+ const outputSchema = tool.outputSchema
61
+ ? usableSchema(tool.outputSchema)
62
+ : undefined;
63
+ input.taken.add(name);
64
+ tools.push({
65
+ capabilityId: input.capabilityId,
66
+ serverName: input.serverName,
67
+ serverToolName: tool.name,
68
+ name,
69
+ ...(typeof tool.description === "string" && tool.description.length > 0
70
+ ? { description: tool.description }
71
+ : {}),
72
+ inputSchema,
73
+ ...(outputSchema === undefined ? {} : { outputSchema }),
74
+ });
75
+ }
76
+ cursor = listed.nextCursor;
77
+ if (!cursor)
78
+ break;
79
+ }
80
+ return { tools, omitted };
81
+ }
82
+ export async function callMcpTool(client, serverToolName, args) {
83
+ const result = await client.callTool({
84
+ name: serverToolName,
85
+ arguments: args && typeof args === "object" && !Array.isArray(args)
86
+ ? args
87
+ : {},
88
+ });
89
+ if ("isError" in result && result.isError) {
90
+ return {
91
+ kind: "failed",
92
+ code: "mcp.tool",
93
+ message: textContent(result.content) || "MCP tool failed",
94
+ };
95
+ }
96
+ if ("structuredContent" in result && result.structuredContent !== undefined)
97
+ return { kind: "completed", output: result.structuredContent };
98
+ const text = "content" in result ? textContent(result.content) : undefined;
99
+ if (text !== undefined)
100
+ return { kind: "completed", output: text };
101
+ return {
102
+ kind: "completed",
103
+ output: "content" in result ? result.content : result,
104
+ };
105
+ }
106
+ export function diagnosticFromError(capabilityId, serverName, error) {
107
+ const credentialIds = error && typeof error === "object" && "credentialIds" in error
108
+ ? error.credentialIds
109
+ : undefined;
110
+ const refused = error instanceof Error && error.message.startsWith("MCP authorization refused");
111
+ if (refused) {
112
+ return {
113
+ capabilityId,
114
+ serverName,
115
+ outcome: "refused",
116
+ message: error.message,
117
+ ...(credentialIds === undefined ? {} : { credentialIds }),
118
+ };
119
+ }
120
+ const code = error && typeof error === "object" && "code" in error
121
+ ? error.code
122
+ : undefined;
123
+ const unauthorized = code === 401 ||
124
+ (error instanceof Error && /\b401\b/.test(error.message));
125
+ return {
126
+ capabilityId,
127
+ serverName,
128
+ outcome: "failed",
129
+ message: unauthorized
130
+ ? "MCP server returned 401"
131
+ : error instanceof Error
132
+ ? error.message.slice(0, 500)
133
+ : "MCP server connection failed",
134
+ };
135
+ }
136
+ function createClient() {
137
+ return new Client({ name: "nylorun-runtime", version: "0.6.0" }, { capabilities: {} });
138
+ }
139
+ function authorizedFetch(server, authorize) {
140
+ return async (url, init) => {
141
+ const target = typeof url === "string" ? url : url.href;
142
+ if (!sameOrigin(target, server.url))
143
+ throw new Error("MCP request origin does not match the declared server");
144
+ const result = await authorize(server.url);
145
+ if (result.status === "refused") {
146
+ const error = new Error(`MCP authorization refused: ${result.reason}`);
147
+ error.credentialIds =
148
+ result.credentialIds;
149
+ throw error;
150
+ }
151
+ const headers = new Headers();
152
+ for (const [key, value] of Object.entries(server.headers ?? {}))
153
+ headers.set(key, value);
154
+ new Headers(init?.headers).forEach((value, key) => headers.set(key, value));
155
+ if (result.status === "authorized")
156
+ headers.set("authorization", result.headers.authorization);
157
+ return fetch(target, { ...init, headers, redirect: "error" });
158
+ };
159
+ }
160
+ function sameOrigin(requestUrl, serverUrl) {
161
+ try {
162
+ return new URL(requestUrl).origin === new URL(serverUrl).origin;
163
+ }
164
+ catch {
165
+ return false;
166
+ }
167
+ }
168
+ function usableSchema(value) {
169
+ if (!value || typeof value !== "object" || Array.isArray(value))
170
+ return undefined;
171
+ const schema = JSON.parse(JSON.stringify(value));
172
+ try {
173
+ schemaFromJSON(schema);
174
+ }
175
+ catch {
176
+ return undefined;
177
+ }
178
+ return schema;
179
+ }
180
+ function textContent(content) {
181
+ if (!Array.isArray(content))
182
+ return undefined;
183
+ const text = content
184
+ .filter((part) => !!part &&
185
+ typeof part === "object" &&
186
+ part.type === "text" &&
187
+ typeof part.text === "string")
188
+ .map((part) => part.text);
189
+ return text.length === 0 ? undefined : text.join("\n");
190
+ }
@@ -0,0 +1,45 @@
1
+ import type { AgentManifest } from "@nylorun/core/define";
2
+ import type { AuthorizeResult } from "../vault/service.js";
3
+ import { callMcpTool } from "./connect.js";
4
+ import { type McpDiagnostic, type McpSnapshot, type McpToolRecord } from "./snapshot.js";
5
+ export declare class McpPool {
6
+ private readonly options;
7
+ private readonly live;
8
+ constructor(options: {
9
+ readonly dataDir: string;
10
+ readonly authorize: (sessionId: string, request: {
11
+ url: string;
12
+ serverName: string;
13
+ }) => Promise<AuthorizeResult>;
14
+ });
15
+ discover(input: {
16
+ sessionId: string;
17
+ manifest: AgentManifest;
18
+ manifestHash: string;
19
+ pluginRoots: Readonly<Record<string, string>>;
20
+ signal?: AbortSignal;
21
+ }): Promise<{
22
+ snapshot: McpSnapshot;
23
+ diagnostics: McpDiagnostic[];
24
+ }>;
25
+ reconnect(input: {
26
+ sessionId: string;
27
+ manifest: AgentManifest;
28
+ pluginRoots: Readonly<Record<string, string>>;
29
+ tools: readonly McpToolRecord[];
30
+ signal?: AbortSignal;
31
+ }): Promise<McpDiagnostic[]>;
32
+ call(input: {
33
+ sessionId: string;
34
+ capabilityId: string;
35
+ serverName: string;
36
+ serverToolName: string;
37
+ args: unknown;
38
+ manifest: AgentManifest;
39
+ pluginRoots: Readonly<Record<string, string>>;
40
+ }): Promise<Awaited<ReturnType<typeof callMcpTool>>>;
41
+ close(): Promise<void>;
42
+ private remember;
43
+ private liveKey;
44
+ private connectDeclared;
45
+ }
@@ -0,0 +1,143 @@
1
+ import { join } from "node:path";
2
+ import { callMcpTool, diagnosticFromError, listMcpTools, openMcpServer, } from "./connect.js";
3
+ import { declaredToolNames, } from "./snapshot.js";
4
+ export class McpPool {
5
+ options;
6
+ live = new Map();
7
+ constructor(options) {
8
+ this.options = options;
9
+ }
10
+ async discover(input) {
11
+ const taken = declaredToolNames(input.manifest);
12
+ const tools = [];
13
+ const diagnostics = [];
14
+ for (const declared of serversOf(input.manifest)) {
15
+ input.signal?.throwIfAborted();
16
+ const opened = await this.connectDeclared(input, declared);
17
+ if (!opened.ok) {
18
+ diagnostics.push(opened.diagnostic);
19
+ continue;
20
+ }
21
+ try {
22
+ const listed = await listMcpTools(opened.connection.client, {
23
+ capabilityId: declared.capabilityId,
24
+ serverName: declared.server.name,
25
+ taken,
26
+ });
27
+ tools.push(...listed.tools);
28
+ diagnostics.push({
29
+ capabilityId: declared.capabilityId,
30
+ serverName: declared.server.name,
31
+ outcome: "connected",
32
+ message: listed.omitted.length === 0
33
+ ? "Connected"
34
+ : `Connected. Omitted ${listed.omitted.join(", ")}`,
35
+ });
36
+ this.remember(input.sessionId, declared, opened.connection);
37
+ }
38
+ catch (error) {
39
+ await opened.connection.close().catch(() => { });
40
+ diagnostics.push(diagnosticFromError(declared.capabilityId, declared.server.name, error));
41
+ }
42
+ }
43
+ return {
44
+ snapshot: {
45
+ snapshotSchemaVersion: 1,
46
+ manifestHash: input.manifestHash,
47
+ mcpTools: tools,
48
+ },
49
+ diagnostics,
50
+ };
51
+ }
52
+ async reconnect(input) {
53
+ const diagnostics = [];
54
+ const seen = new Set();
55
+ for (const tool of input.tools) {
56
+ const key = `${tool.capabilityId}\0${tool.serverName}`;
57
+ if (seen.has(key))
58
+ continue;
59
+ seen.add(key);
60
+ if (this.live.has(this.liveKey(input.sessionId, tool.capabilityId, tool.serverName)))
61
+ continue;
62
+ input.signal?.throwIfAborted();
63
+ const declared = serversOf(input.manifest).find((item) => item.capabilityId === tool.capabilityId && item.server.name === tool.serverName);
64
+ if (!declared) {
65
+ diagnostics.push({
66
+ capabilityId: tool.capabilityId,
67
+ serverName: tool.serverName,
68
+ outcome: "failed",
69
+ message: "Declared MCP server is missing from the pinned manifest",
70
+ });
71
+ continue;
72
+ }
73
+ const opened = await this.connectDeclared(input, declared);
74
+ if (!opened.ok) {
75
+ diagnostics.push(opened.diagnostic);
76
+ continue;
77
+ }
78
+ this.remember(input.sessionId, declared, opened.connection);
79
+ }
80
+ return diagnostics;
81
+ }
82
+ async call(input) {
83
+ const key = this.liveKey(input.sessionId, input.capabilityId, input.serverName);
84
+ let live = this.live.get(key);
85
+ if (!live) {
86
+ const declared = serversOf(input.manifest).find((item) => item.capabilityId === input.capabilityId && item.server.name === input.serverName);
87
+ if (!declared)
88
+ throw new Error(`MCP server '${input.serverName}' is not declared`);
89
+ const opened = await this.connectDeclared(input, declared);
90
+ if (!opened.ok)
91
+ throw new Error(opened.diagnostic.message);
92
+ this.remember(input.sessionId, declared, opened.connection);
93
+ live = this.live.get(key);
94
+ }
95
+ if (!live)
96
+ throw new Error(`MCP server '${input.serverName}' is not connected`);
97
+ return callMcpTool(live.connection.client, input.serverToolName, input.args);
98
+ }
99
+ async close() {
100
+ const connections = [...this.live.values()];
101
+ this.live.clear();
102
+ await Promise.all(connections.map((item) => item.connection.close().catch(() => { })));
103
+ }
104
+ remember(sessionId, declared, connection) {
105
+ this.live.set(this.liveKey(sessionId, declared.capabilityId, declared.server.name), {
106
+ capabilityId: declared.capabilityId,
107
+ serverName: declared.server.name,
108
+ connection,
109
+ });
110
+ }
111
+ liveKey(sessionId, capabilityId, serverName) {
112
+ return JSON.stringify([sessionId, capabilityId, serverName]);
113
+ }
114
+ async connectDeclared(input, declared) {
115
+ try {
116
+ const connection = await openMcpServer({
117
+ server: declared.server,
118
+ pluginRoot: input.pluginRoots[declared.capabilityId],
119
+ pluginData: join(this.options.dataDir, "plugin-data", declared.capabilityId),
120
+ authorize: declared.server.type === "stdio"
121
+ ? undefined
122
+ : (url) => this.options.authorize(input.sessionId, {
123
+ url,
124
+ serverName: declared.server.name,
125
+ }),
126
+ });
127
+ return { ok: true, connection };
128
+ }
129
+ catch (error) {
130
+ return {
131
+ ok: false,
132
+ diagnostic: diagnosticFromError(declared.capabilityId, declared.server.name, error),
133
+ };
134
+ }
135
+ }
136
+ }
137
+ function serversOf(manifest) {
138
+ const servers = [];
139
+ for (const capability of manifest.capabilities)
140
+ for (const server of Object.values(capability.mcpServers ?? {}))
141
+ servers.push({ capabilityId: capability.id, server });
142
+ return servers;
143
+ }
@@ -0,0 +1,25 @@
1
+ import type { JsonObject } from "@nylorun/core/define";
2
+ import type { AgentManifest } from "@nylorun/core/define";
3
+ export interface McpToolRecord {
4
+ readonly capabilityId: string;
5
+ readonly serverName: string;
6
+ readonly serverToolName: string;
7
+ readonly name: string;
8
+ readonly description?: string;
9
+ readonly inputSchema: JsonObject;
10
+ readonly outputSchema?: JsonObject;
11
+ }
12
+ export interface McpSnapshot {
13
+ readonly snapshotSchemaVersion: 1;
14
+ readonly manifestHash: string;
15
+ readonly mcpTools: readonly McpToolRecord[];
16
+ }
17
+ export interface McpDiagnostic {
18
+ readonly capabilityId: string;
19
+ readonly serverName: string;
20
+ readonly outcome: "connected" | "refused" | "failed";
21
+ readonly message: string;
22
+ readonly credentialIds?: readonly string[];
23
+ }
24
+ export declare function modelToolName(serverName: string, serverToolName: string): string;
25
+ export declare function declaredToolNames(manifest: AgentManifest): Set<string>;
@@ -0,0 +1,10 @@
1
+ export function modelToolName(serverName, serverToolName) {
2
+ return `${serverName}__${serverToolName}`;
3
+ }
4
+ export function declaredToolNames(manifest) {
5
+ const names = new Set();
6
+ for (const capability of manifest.capabilities)
7
+ for (const tool of capability.tools ?? [])
8
+ names.add(tool.name);
9
+ return names;
10
+ }
@@ -0,0 +1,15 @@
1
+ import type { McpServerManifest } from "@nylorun/core/define";
2
+ /** Single-pass replacement. Text inserted for a token is not scanned again. */
3
+ export declare function expandPluginPlaceholders(value: string, pluginRoot: string, pluginData: string): string;
4
+ export interface StdioLaunch {
5
+ readonly command: string;
6
+ readonly args: readonly string[];
7
+ readonly cwd: string;
8
+ readonly env: Readonly<Record<string, string>>;
9
+ }
10
+ export declare function prepareStdioLaunch(server: Extract<McpServerManifest, {
11
+ type: "stdio";
12
+ }>, options: {
13
+ readonly pluginRoot?: string;
14
+ readonly pluginData: string;
15
+ }): StdioLaunch;