@nylorun/runtime 0.5.0-beta → 0.7.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 +45 -0
- package/README.md +30 -60
- package/dist/config.d.ts +1 -1
- package/dist/configuration.d.ts +3 -0
- package/dist/configuration.js +3 -0
- package/dist/contracts.d.ts +4 -2
- package/dist/core/main.js +40 -0
- package/dist/core/provider.d.ts +12 -0
- package/dist/core/provider.js +60 -0
- package/dist/core/runtime.d.ts +67 -0
- package/dist/core/runtime.js +1126 -0
- package/dist/core/store.d.ts +17 -0
- package/dist/core/store.js +146 -0
- package/dist/index.d.ts +6 -11
- package/dist/index.js +4 -6
- package/dist/mcp/connect.d.ts +31 -0
- package/dist/mcp/connect.js +190 -0
- package/dist/mcp/pool.d.ts +45 -0
- package/dist/mcp/pool.js +143 -0
- package/dist/mcp/snapshot.d.ts +25 -0
- package/dist/mcp/snapshot.js +10 -0
- package/dist/mcp/stdio.d.ts +15 -0
- package/dist/mcp/stdio.js +80 -0
- package/dist/model/defaults.d.ts +1 -1
- package/dist/model/http-model.d.ts +1 -1
- package/dist/node/local-sessions.js +17 -0
- package/dist/server/host.d.ts +42 -4
- package/dist/server/host.js +69 -3
- package/dist/session/api.d.ts +10 -0
- package/dist/session/api.js +15 -0
- package/dist/session/default.d.ts +5 -0
- package/dist/session/default.js +30 -0
- package/dist/session/handle.d.ts +27 -0
- package/dist/session/handle.js +199 -0
- package/dist/session/index.d.ts +2 -0
- package/dist/session/index.js +2 -0
- package/dist/sessions/host.d.ts +11 -1
- package/dist/sessions/host.js +75 -7
- package/dist/sessions/store.d.ts +1 -0
- package/dist/sessions/store.js +3 -0
- package/dist/vault/crypto.d.ts +12 -0
- package/dist/vault/crypto.js +80 -0
- package/dist/vault/error.d.ts +4 -0
- package/dist/vault/error.js +7 -0
- package/dist/vault/kek.d.ts +6 -0
- package/dist/vault/kek.js +26 -0
- package/dist/vault/service.d.ts +59 -0
- package/dist/vault/service.js +538 -0
- package/dist/vault/url.d.ts +3 -0
- package/dist/vault/url.js +27 -0
- package/package.json +19 -13
- package/dist/cli.d.ts +0 -2
- package/dist/cli.js +0 -100
- package/dist/dev-entry.js +0 -2
- package/dist/dev.d.ts +0 -2
- package/dist/dev.js +0 -126
- package/dist/environment.d.ts +0 -2
- package/dist/environment.js +0 -64
- package/dist/launcher.d.ts +0 -1
- package/dist/launcher.js +0 -33
- package/dist/model/configure.d.ts +0 -12
- package/dist/model/configure.js +0 -155
- /package/dist/{dev-entry.d.ts → core/main.d.ts} +0 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { DatabaseSync } from "node:sqlite";
|
|
2
|
+
import type { LiveEvent } from "@nylorun/core/contracts";
|
|
3
|
+
export declare function canonical(value: unknown): string;
|
|
4
|
+
export declare class Store {
|
|
5
|
+
readonly db: DatabaseSync;
|
|
6
|
+
constructor(path: string);
|
|
7
|
+
tx<T>(fn: () => T): T;
|
|
8
|
+
get<T = any>(table: string, id: string): T | undefined;
|
|
9
|
+
all<T = any>(table: string): T[];
|
|
10
|
+
put(table: string, id: string, body: unknown): void;
|
|
11
|
+
event(sessionId: string, turnId: string | null, type: string, payload: unknown): LiveEvent;
|
|
12
|
+
credentialCount(): number;
|
|
13
|
+
history(sessionId: string, cursor?: string): {
|
|
14
|
+
items: LiveEvent[];
|
|
15
|
+
cursor: string | null;
|
|
16
|
+
};
|
|
17
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { DatabaseSync } from "node:sqlite";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
export function canonical(value) {
|
|
4
|
+
if (Array.isArray(value))
|
|
5
|
+
return "[" + value.map(canonical).join(",") + "]";
|
|
6
|
+
if (value && typeof value === "object")
|
|
7
|
+
return ("{" +
|
|
8
|
+
Object.keys(value)
|
|
9
|
+
.sort()
|
|
10
|
+
.filter((k) => value[k] !== undefined)
|
|
11
|
+
.map((k) => JSON.stringify(k) + ":" + canonical(value[k]))
|
|
12
|
+
.join(",") +
|
|
13
|
+
"}");
|
|
14
|
+
return JSON.stringify(value) ?? "null";
|
|
15
|
+
}
|
|
16
|
+
export class Store {
|
|
17
|
+
db;
|
|
18
|
+
constructor(path) {
|
|
19
|
+
this.db = new DatabaseSync(path);
|
|
20
|
+
this.db
|
|
21
|
+
.exec(`PRAGMA journal_mode=WAL; PRAGMA foreign_keys=ON; PRAGMA busy_timeout=5000;
|
|
22
|
+
CREATE TABLE IF NOT EXISTS definitions(id TEXT PRIMARY KEY, body TEXT NOT NULL);
|
|
23
|
+
CREATE TABLE IF NOT EXISTS sessions(id TEXT PRIMARY KEY, body TEXT NOT NULL);
|
|
24
|
+
CREATE TABLE IF NOT EXISTS commands(id TEXT PRIMARY KEY, body TEXT NOT NULL);
|
|
25
|
+
CREATE TABLE IF NOT EXISTS checkpoints(id TEXT PRIMARY KEY, body TEXT NOT NULL);
|
|
26
|
+
CREATE TABLE IF NOT EXISTS effects(id TEXT PRIMARY KEY, body TEXT NOT NULL);
|
|
27
|
+
CREATE TABLE IF NOT EXISTS actions(id TEXT PRIMARY KEY, body TEXT NOT NULL);
|
|
28
|
+
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 TABLE IF NOT EXISTS vaults(
|
|
31
|
+
id TEXT PRIMARY KEY,
|
|
32
|
+
name TEXT NOT NULL,
|
|
33
|
+
owner_user_id TEXT NOT NULL,
|
|
34
|
+
metadata_json TEXT,
|
|
35
|
+
created_at TEXT NOT NULL
|
|
36
|
+
);
|
|
37
|
+
CREATE INDEX IF NOT EXISTS vaults_owner ON vaults(owner_user_id);
|
|
38
|
+
CREATE TABLE IF NOT EXISTS vault_credentials(
|
|
39
|
+
id TEXT PRIMARY KEY,
|
|
40
|
+
vault_id TEXT NOT NULL REFERENCES vaults(id) ON DELETE CASCADE,
|
|
41
|
+
name TEXT NOT NULL,
|
|
42
|
+
type TEXT NOT NULL,
|
|
43
|
+
binding_json TEXT NOT NULL,
|
|
44
|
+
expires_at TEXT,
|
|
45
|
+
created_at TEXT NOT NULL,
|
|
46
|
+
rotated_at TEXT,
|
|
47
|
+
kek_id TEXT NOT NULL,
|
|
48
|
+
nonce BLOB NOT NULL,
|
|
49
|
+
ciphertext BLOB NOT NULL,
|
|
50
|
+
wrapped_dek BLOB NOT NULL
|
|
51
|
+
);
|
|
52
|
+
CREATE INDEX IF NOT EXISTS vault_credentials_vault ON vault_credentials(vault_id);
|
|
53
|
+
CREATE TABLE IF NOT EXISTS vault_audit(
|
|
54
|
+
id TEXT PRIMARY KEY,
|
|
55
|
+
at TEXT NOT NULL,
|
|
56
|
+
actor TEXT NOT NULL,
|
|
57
|
+
action TEXT NOT NULL,
|
|
58
|
+
vault_id TEXT,
|
|
59
|
+
credential_id TEXT,
|
|
60
|
+
session_id TEXT,
|
|
61
|
+
target TEXT,
|
|
62
|
+
outcome TEXT NOT NULL
|
|
63
|
+
);
|
|
64
|
+
CREATE TABLE IF NOT EXISTS vault_idempotency(
|
|
65
|
+
id TEXT PRIMARY KEY,
|
|
66
|
+
body_hash TEXT NOT NULL,
|
|
67
|
+
response TEXT NOT NULL
|
|
68
|
+
);`);
|
|
69
|
+
}
|
|
70
|
+
tx(fn) {
|
|
71
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
72
|
+
try {
|
|
73
|
+
const result = fn();
|
|
74
|
+
this.db.exec("COMMIT");
|
|
75
|
+
return result;
|
|
76
|
+
}
|
|
77
|
+
catch (e) {
|
|
78
|
+
this.db.exec("ROLLBACK");
|
|
79
|
+
throw e;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
get(table, id) {
|
|
83
|
+
const row = this.db.prepare(`SELECT body FROM ${table} WHERE id=?`).get(id);
|
|
84
|
+
return row ? JSON.parse(String(row.body)) : undefined;
|
|
85
|
+
}
|
|
86
|
+
all(table) {
|
|
87
|
+
return this.db
|
|
88
|
+
.prepare(`SELECT body FROM ${table}`)
|
|
89
|
+
.all()
|
|
90
|
+
.map((r) => JSON.parse(String(r.body)));
|
|
91
|
+
}
|
|
92
|
+
put(table, id, body) {
|
|
93
|
+
this.db
|
|
94
|
+
.prepare(`INSERT INTO ${table}(id,body) VALUES(?,?) ON CONFLICT(id) DO UPDATE SET body=excluded.body`)
|
|
95
|
+
.run(id, JSON.stringify(body));
|
|
96
|
+
}
|
|
97
|
+
event(sessionId, turnId, type, payload) {
|
|
98
|
+
const base = {
|
|
99
|
+
eventId: randomUUID(),
|
|
100
|
+
sessionId,
|
|
101
|
+
turnId,
|
|
102
|
+
createdAt: new Date().toISOString(),
|
|
103
|
+
type,
|
|
104
|
+
payload,
|
|
105
|
+
};
|
|
106
|
+
const result = this.db
|
|
107
|
+
.prepare("INSERT INTO events(session_id,body) VALUES(?,?)")
|
|
108
|
+
.run(sessionId, "{}");
|
|
109
|
+
const event = {
|
|
110
|
+
...base,
|
|
111
|
+
cursor: Buffer.from(`${sessionId}:${result.lastInsertRowid}`).toString("base64url"),
|
|
112
|
+
};
|
|
113
|
+
this.db
|
|
114
|
+
.prepare("UPDATE events SET body=? WHERE sequence=?")
|
|
115
|
+
.run(JSON.stringify(event), result.lastInsertRowid);
|
|
116
|
+
return event;
|
|
117
|
+
}
|
|
118
|
+
credentialCount() {
|
|
119
|
+
const row = this.db
|
|
120
|
+
.prepare("SELECT COUNT(*) AS n FROM vault_credentials")
|
|
121
|
+
.get();
|
|
122
|
+
return Number(row.n);
|
|
123
|
+
}
|
|
124
|
+
history(sessionId, cursor) {
|
|
125
|
+
let sequence = 0;
|
|
126
|
+
if (cursor) {
|
|
127
|
+
const decoded = Buffer.from(cursor, "base64url").toString();
|
|
128
|
+
const prefix = `${sessionId}:`;
|
|
129
|
+
if (!decoded.startsWith(prefix) ||
|
|
130
|
+
!/^\d+$/.test(decoded.slice(prefix.length)))
|
|
131
|
+
throw new Error("Invalid cursor");
|
|
132
|
+
sequence = Number(decoded.slice(prefix.length));
|
|
133
|
+
}
|
|
134
|
+
const items = this.db
|
|
135
|
+
.prepare("SELECT body FROM events WHERE session_id=? AND sequence>? ORDER BY sequence")
|
|
136
|
+
.all(sessionId, sequence)
|
|
137
|
+
.map((r) => JSON.parse(String(r.body)));
|
|
138
|
+
const last = this.db
|
|
139
|
+
.prepare("SELECT body FROM events WHERE session_id=? ORDER BY sequence DESC LIMIT 1")
|
|
140
|
+
.get(sessionId);
|
|
141
|
+
return {
|
|
142
|
+
items,
|
|
143
|
+
cursor: last ? JSON.parse(String(last.body)).cursor : null,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,11 +1,6 @@
|
|
|
1
|
-
export type
|
|
2
|
-
export type
|
|
3
|
-
export {
|
|
4
|
-
export {
|
|
5
|
-
export {
|
|
6
|
-
export type {
|
|
7
|
-
export { IMAGE_MEDIA_TYPES, MAX_IMAGE_BYTES, decodeImageBase64, validateImageBytes, } from "./media.js";
|
|
8
|
-
export type { RuntimeMedia, MediaAsset, MediaReference } from "./media.js";
|
|
9
|
-
export { httpModel, type HttpModelOptions, type ModelEnvironment, } from "./model/http-model.js";
|
|
10
|
-
export { agUiEvents } from "./server/ag-ui.js";
|
|
11
|
-
export type { ModelFactoryOptions, ModelPreview } from "./model/defaults.js";
|
|
1
|
+
export { CoreRuntime, createRuntime, startRuntime, type RuntimeOptions } from "./core/runtime.js";
|
|
2
|
+
export { scriptedModel, gatewayModel, type ModelProvider } from "./core/provider.js";
|
|
3
|
+
export { httpModel, type HttpModelOptions, type ModelEnvironment } from "./model/http-model.js";
|
|
4
|
+
export type { RuntimeAgent } from "./contracts.js";
|
|
5
|
+
export { IMAGE_MEDIA_TYPES, MAX_IMAGE_BYTES, decodeImageBase64, validateImageBytes } from "./media.js";
|
|
6
|
+
export type { RuntimeModelAdapter } from "./contracts.js";
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
export {
|
|
2
|
-
export {
|
|
3
|
-
export {
|
|
4
|
-
export { IMAGE_MEDIA_TYPES, MAX_IMAGE_BYTES, decodeImageBase64, validateImageBytes
|
|
5
|
-
export { httpModel, } from "./model/http-model.js";
|
|
6
|
-
export { agUiEvents } from "./server/ag-ui.js";
|
|
1
|
+
export { CoreRuntime, createRuntime, startRuntime } from "./core/runtime.js";
|
|
2
|
+
export { scriptedModel, gatewayModel } from "./core/provider.js";
|
|
3
|
+
export { httpModel } from "./model/http-model.js";
|
|
4
|
+
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
|
+
}
|
package/dist/mcp/pool.js
ADDED
|
@@ -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;
|