@ryuhq/sdk 0.1.14 → 0.2.0
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/README.md +19 -6
- package/dist/agent-plugin.cjs +46 -0
- package/dist/agent-plugin.d.cts +42 -32
- package/dist/agent-plugin.d.ts +42 -32
- package/dist/agent-plugin.js +1 -1
- package/dist/agent.cjs +9 -0
- package/dist/agent.d.cts +377 -1
- package/dist/agent.d.ts +377 -1
- package/dist/agent.js +4 -2
- package/dist/app-Bkw7LlCK.d.ts +129 -0
- package/dist/app-DNaGmLVf.d.cts +129 -0
- package/dist/builder.cjs +1095 -0
- package/dist/builder.d.cts +212 -0
- package/dist/builder.d.ts +212 -0
- package/dist/builder.js +28 -0
- package/dist/chunk-A3RGEPDG.js +250 -0
- package/dist/chunk-FZSFZOIN.js +200 -0
- package/dist/{chunk-ODFEUVPW.js → chunk-HACAGK65.js} +1 -18
- package/dist/{chunk-AO2KJRDD.js → chunk-IEUQ3CDG.js} +125 -2
- package/dist/chunk-IKEDLLFY.js +19 -0
- package/dist/chunk-IOLP5FFE.js +354 -0
- package/dist/{chunk-G6FLVEC4.js → chunk-JX6DDRXV.js} +46 -0
- package/dist/chunk-T5676WL2.js +240 -0
- package/dist/chunk-TLDPEGC7.js +21 -0
- package/dist/chunk-TXSHHZF2.js +0 -0
- package/dist/{chunk-MTUBUPIV.js → chunk-ULSVL7EC.js} +8 -227
- package/dist/chunk-VLIRNNAE.js +154 -0
- package/dist/chunk-W3KPP4WN.js +135 -0
- package/dist/cli.cjs +258 -14
- package/dist/cli.js +96 -16
- package/dist/client-D5U6ssPc.d.cts +84 -0
- package/dist/client-D5U6ssPc.d.ts +84 -0
- package/dist/index.cjs +519 -5
- package/dist/index.d.cts +16 -632
- package/dist/index.d.ts +16 -632
- package/dist/index.js +63 -695
- package/dist/manifest.cjs +129 -3
- package/dist/manifest.d.cts +105 -2
- package/dist/manifest.d.ts +105 -2
- package/dist/manifest.js +7 -1
- package/dist/mcp/client.cjs +180 -0
- package/dist/mcp/client.d.cts +49 -0
- package/dist/mcp/client.d.ts +49 -0
- package/dist/mcp/client.js +10 -0
- package/dist/mcp/server.cjs +370 -0
- package/dist/mcp/server.d.cts +126 -0
- package/dist/mcp/server.d.ts +126 -0
- package/dist/mcp/server.js +9 -0
- package/dist/mcp.cjs +376 -0
- package/dist/mcp.d.cts +2 -0
- package/dist/mcp.d.ts +2 -0
- package/dist/mcp.js +17 -0
- package/dist/model.cjs +141 -0
- package/dist/model.d.cts +33 -0
- package/dist/model.d.ts +33 -0
- package/dist/model.js +18 -0
- package/dist/plugin.cjs +46 -0
- package/dist/plugin.d.cts +215 -0
- package/dist/plugin.d.ts +215 -0
- package/dist/plugin.js +8 -0
- package/dist/runnable.cjs +1230 -0
- package/dist/runnable.d.cts +271 -0
- package/dist/runnable.d.ts +271 -0
- package/dist/runnable.js +28 -0
- package/dist/{index-B6SkaAjJ.d.ts → tool-DSx2bFx8.d.ts} +35 -458
- package/dist/{index-BvAB5eMk.d.cts → tool-u-VR0fLF.d.cts} +35 -458
- package/package.json +38 -2
- package/src/agent/loop.test.ts +4 -4
- package/src/agent/tools.ts +3 -3
- package/src/agent-plugin.test.ts +58 -0
- package/src/agent-plugin.ts +108 -35
- package/src/cli/dev.test.ts +26 -47
- package/src/cli/dev.ts +10 -2
- package/src/cli.ts +98 -15
- package/src/exports-lockstep.test.ts +92 -0
- package/src/generated/plugin-manifest.ts +243 -27
- package/src/index.ts +35 -0
- package/src/manifest-schema.test.ts +30 -1
- package/src/manifest.fixtures.test.ts +13 -3
- package/src/manifest.test.ts +96 -10
- package/src/manifest.ts +334 -187
- package/src/mcp/index.ts +18 -0
- package/src/model/index.ts +22 -0
- package/src/plugin/ryu-plugin.ts +82 -0
- package/src/runnable/app.test.ts +2 -0
- package/src/runnable/app.ts +5 -2
- package/src/runnable/index.ts +2 -0
- package/src/runnable/primitives.ts +57 -0
- package/src/runnable/tool.ts +1 -1
- package/src/runnable/turn-hook.ts +4 -1
|
@@ -1,231 +1,15 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createPrimitives,
|
|
3
|
+
httpPrimitiveTransport
|
|
4
|
+
} from "./chunk-A3RGEPDG.js";
|
|
5
|
+
import {
|
|
6
|
+
defineModel
|
|
7
|
+
} from "./chunk-HACAGK65.js";
|
|
1
8
|
import {
|
|
2
9
|
assertAllowedEgressUrl,
|
|
3
|
-
defineModel,
|
|
4
10
|
resolveGatewayToken,
|
|
5
11
|
resolveGatewayUrl
|
|
6
|
-
} from "./chunk-
|
|
7
|
-
|
|
8
|
-
// src/runnable/primitives.ts
|
|
9
|
-
function dataUrlToBytes(dataUrl) {
|
|
10
|
-
const match = /^data:([^;,]*)(;base64)?,([\s\S]*)$/.exec(dataUrl);
|
|
11
|
-
if (!match) {
|
|
12
|
-
throw new Error(
|
|
13
|
-
"stt.transcribe expects an `audio` value that is a data: URL (data:<mime>;base64,<data>)"
|
|
14
|
-
);
|
|
15
|
-
}
|
|
16
|
-
const mediaType = match[1] || "application/octet-stream";
|
|
17
|
-
const isBase64 = Boolean(match[2]);
|
|
18
|
-
const payload = match[3] ?? "";
|
|
19
|
-
if (isBase64) {
|
|
20
|
-
const binary = atob(payload);
|
|
21
|
-
const bytes = new Uint8Array(binary.length);
|
|
22
|
-
for (let i = 0; i < binary.length; i++) {
|
|
23
|
-
bytes[i] = binary.charCodeAt(i);
|
|
24
|
-
}
|
|
25
|
-
return { bytes, mediaType };
|
|
26
|
-
}
|
|
27
|
-
return {
|
|
28
|
-
bytes: new TextEncoder().encode(decodeURIComponent(payload)),
|
|
29
|
-
mediaType
|
|
30
|
-
};
|
|
31
|
-
}
|
|
32
|
-
function bytesToDataUrl(bytes, mediaType) {
|
|
33
|
-
let binary = "";
|
|
34
|
-
const chunk = 32768;
|
|
35
|
-
for (let i = 0; i < bytes.length; i += chunk) {
|
|
36
|
-
binary += String.fromCharCode(...bytes.subarray(i, i + chunk));
|
|
37
|
-
}
|
|
38
|
-
return `data:${mediaType};base64,${btoa(binary)}`;
|
|
39
|
-
}
|
|
40
|
-
function httpPrimitiveTransport(options) {
|
|
41
|
-
const base = options.nodeUrl.replace(/\/+$/, "");
|
|
42
|
-
assertAllowedEgressUrl(base);
|
|
43
|
-
const doFetch = options.fetchImpl ?? fetch;
|
|
44
|
-
const authHeader = () => options.token ? { authorization: `Bearer ${options.token}` } : {};
|
|
45
|
-
const failDetail = async (path, res) => {
|
|
46
|
-
const detail = await res.text().catch(() => "");
|
|
47
|
-
return new Error(
|
|
48
|
-
`Ryu primitive call ${path} failed: ${res.status} ${res.statusText}${detail ? ` \u2014 ${detail}` : ""}`
|
|
49
|
-
);
|
|
50
|
-
};
|
|
51
|
-
const post = async (path, body) => {
|
|
52
|
-
const res = await doFetch(`${base}${path}`, {
|
|
53
|
-
method: "POST",
|
|
54
|
-
headers: { "content-type": "application/json", ...authHeader() },
|
|
55
|
-
body: JSON.stringify(body ?? {})
|
|
56
|
-
});
|
|
57
|
-
if (!res.ok) {
|
|
58
|
-
throw await failDetail(path, res);
|
|
59
|
-
}
|
|
60
|
-
const text = await res.text();
|
|
61
|
-
return text ? JSON.parse(text) : void 0;
|
|
62
|
-
};
|
|
63
|
-
const transcribeDirect = async (body) => {
|
|
64
|
-
const input = body ?? {};
|
|
65
|
-
if (!input.audio) {
|
|
66
|
-
throw new Error(
|
|
67
|
-
"stt.transcribe requires an `audio` data: URL (the recorded audio)"
|
|
68
|
-
);
|
|
69
|
-
}
|
|
70
|
-
const { bytes, mediaType } = dataUrlToBytes(input.audio);
|
|
71
|
-
const form = new FormData();
|
|
72
|
-
form.append(
|
|
73
|
-
"file",
|
|
74
|
-
new Blob([bytes], { type: mediaType || "audio/wav" }),
|
|
75
|
-
input.filename ?? "recording.wav"
|
|
76
|
-
);
|
|
77
|
-
const res = await doFetch(`${base}/api/voice/transcribe`, {
|
|
78
|
-
method: "POST",
|
|
79
|
-
headers: authHeader(),
|
|
80
|
-
body: form
|
|
81
|
-
});
|
|
82
|
-
if (!res.ok) {
|
|
83
|
-
throw await failDetail("/api/voice/transcribe", res);
|
|
84
|
-
}
|
|
85
|
-
const parsed = await res.json();
|
|
86
|
-
return (parsed.text ?? "").trim();
|
|
87
|
-
};
|
|
88
|
-
const speakDirect = async (body) => {
|
|
89
|
-
const res = await doFetch(`${base}/api/voice/speak`, {
|
|
90
|
-
method: "POST",
|
|
91
|
-
headers: { "content-type": "application/json", ...authHeader() },
|
|
92
|
-
body: JSON.stringify(body ?? {})
|
|
93
|
-
});
|
|
94
|
-
if (!res.ok) {
|
|
95
|
-
throw await failDetail("/api/voice/speak", res);
|
|
96
|
-
}
|
|
97
|
-
const bytes = new Uint8Array(await res.arrayBuffer());
|
|
98
|
-
const mediaType = res.headers.get("content-type") || "audio/wav";
|
|
99
|
-
return bytesToDataUrl(bytes, mediaType);
|
|
100
|
-
};
|
|
101
|
-
const generateImageDirect = async (body) => {
|
|
102
|
-
const res = await doFetch(`${base}/api/images/generate`, {
|
|
103
|
-
method: "POST",
|
|
104
|
-
headers: { "content-type": "application/json", ...authHeader() },
|
|
105
|
-
body: JSON.stringify(body ?? {})
|
|
106
|
-
});
|
|
107
|
-
if (!res.ok) {
|
|
108
|
-
throw await failDetail("/api/images/generate", res);
|
|
109
|
-
}
|
|
110
|
-
const parsed = await res.json();
|
|
111
|
-
return (parsed.data ?? []).map(
|
|
112
|
-
(item) => item.url ? item.url : `data:image/png;base64,${item.b64_json ?? ""}`
|
|
113
|
-
);
|
|
114
|
-
};
|
|
115
|
-
return {
|
|
116
|
-
bridge(method, args) {
|
|
117
|
-
if (!options.pluginId) {
|
|
118
|
-
return Promise.reject(
|
|
119
|
-
new Error(
|
|
120
|
-
`bridge primitive "${method}" requires a pluginId (the /api/plugins/:id/host caller identity)`
|
|
121
|
-
)
|
|
122
|
-
);
|
|
123
|
-
}
|
|
124
|
-
return post(`/api/plugins/${options.pluginId}/host`, { method, args });
|
|
125
|
-
},
|
|
126
|
-
direct(path, body) {
|
|
127
|
-
if (path === "/api/voice/transcribe") {
|
|
128
|
-
return transcribeDirect(body);
|
|
129
|
-
}
|
|
130
|
-
if (path === "/api/voice/speak") {
|
|
131
|
-
return speakDirect(body);
|
|
132
|
-
}
|
|
133
|
-
if (path === "/api/images/generate") {
|
|
134
|
-
return generateImageDirect(body);
|
|
135
|
-
}
|
|
136
|
-
return post(path, body);
|
|
137
|
-
},
|
|
138
|
-
capability(cap, body) {
|
|
139
|
-
return post(`/api/host/capability/${cap}`, body);
|
|
140
|
-
}
|
|
141
|
-
};
|
|
142
|
-
}
|
|
143
|
-
var PRIMITIVE_BINDINGS = {
|
|
144
|
-
// Bridge families (existing `PluginHookBridge`).
|
|
145
|
-
"engines.complete": {
|
|
146
|
-
transport: "bridge",
|
|
147
|
-
method: "model.complete",
|
|
148
|
-
grant: "hook:side-model"
|
|
149
|
-
},
|
|
150
|
-
// Host-direct media data-path (the host holds the node token; returns data: URLs).
|
|
151
|
-
"image.generate": {
|
|
152
|
-
transport: "direct",
|
|
153
|
-
path: "/api/images/generate",
|
|
154
|
-
grant: "media:generate"
|
|
155
|
-
},
|
|
156
|
-
"tts.speak": {
|
|
157
|
-
transport: "direct",
|
|
158
|
-
path: "/api/voice/speak",
|
|
159
|
-
grant: "media:generate"
|
|
160
|
-
},
|
|
161
|
-
"stt.transcribe": {
|
|
162
|
-
transport: "direct",
|
|
163
|
-
path: "/api/voice/transcribe",
|
|
164
|
-
grant: "media:transcribe"
|
|
165
|
-
},
|
|
166
|
-
// Broker capabilities — no rpc family yet (@requires-grant).
|
|
167
|
-
"rag.retrieve": { transport: "broker", capability: "rag" },
|
|
168
|
-
"rag.embed": { transport: "broker", capability: "rag" },
|
|
169
|
-
"rag.rerank": { transport: "broker", capability: "rag" },
|
|
170
|
-
"memory.recall": { transport: "broker", capability: "memory" },
|
|
171
|
-
"memory.store": { transport: "broker", capability: "memory" },
|
|
172
|
-
"realtime.broadcast": { transport: "broker", capability: "realtime" },
|
|
173
|
-
"realtime.subscribe": { transport: "broker", capability: "realtime" },
|
|
174
|
-
"durable.checkpoint": { transport: "broker", capability: "durable" },
|
|
175
|
-
"durable.resume": { transport: "broker", capability: "durable" },
|
|
176
|
-
"engines.embed": { transport: "broker", capability: "engines" }
|
|
177
|
-
};
|
|
178
|
-
function brokerCall(transport, cap, op, input) {
|
|
179
|
-
return transport.capability(cap, { op, input });
|
|
180
|
-
}
|
|
181
|
-
function createPrimitives(transport) {
|
|
182
|
-
return {
|
|
183
|
-
rag: {
|
|
184
|
-
retrieve: (input) => brokerCall(transport, "rag", "retrieve", input),
|
|
185
|
-
embed: (input) => brokerCall(transport, "rag", "embed", input),
|
|
186
|
-
rerank: (input) => brokerCall(transport, "rag", "rerank", input)
|
|
187
|
-
},
|
|
188
|
-
memory: {
|
|
189
|
-
recall: (input) => brokerCall(transport, "memory", "recall", input),
|
|
190
|
-
store: (input) => brokerCall(transport, "memory", "store", input)
|
|
191
|
-
},
|
|
192
|
-
realtime: {
|
|
193
|
-
broadcast: (input) => brokerCall(transport, "realtime", "broadcast", input).then(
|
|
194
|
-
() => void 0
|
|
195
|
-
),
|
|
196
|
-
subscribe: (input) => brokerCall(
|
|
197
|
-
transport,
|
|
198
|
-
"realtime",
|
|
199
|
-
"subscribe",
|
|
200
|
-
input
|
|
201
|
-
)
|
|
202
|
-
},
|
|
203
|
-
durable: {
|
|
204
|
-
checkpoint: (input) => brokerCall(transport, "durable", "checkpoint", input),
|
|
205
|
-
resume: (input) => brokerCall(transport, "durable", "resume", input)
|
|
206
|
-
},
|
|
207
|
-
engines: {
|
|
208
|
-
// Bridge family: model.complete → host.sideModel (wire keys are snake_case).
|
|
209
|
-
complete: (input) => transport.bridge("model.complete", {
|
|
210
|
-
prompt: input.prompt,
|
|
211
|
-
system: input.system,
|
|
212
|
-
model: input.model,
|
|
213
|
-
model_pref_key: input.modelPrefKey,
|
|
214
|
-
effort: input.effort
|
|
215
|
-
}),
|
|
216
|
-
embed: (input) => brokerCall(transport, "engines", "embed", input)
|
|
217
|
-
},
|
|
218
|
-
tts: {
|
|
219
|
-
speak: (input) => transport.direct("/api/voice/speak", input)
|
|
220
|
-
},
|
|
221
|
-
stt: {
|
|
222
|
-
transcribe: (input) => transport.direct("/api/voice/transcribe", input)
|
|
223
|
-
},
|
|
224
|
-
image: {
|
|
225
|
-
generate: (input) => transport.direct("/api/images/generate", input)
|
|
226
|
-
}
|
|
227
|
-
};
|
|
228
|
-
}
|
|
12
|
+
} from "./chunk-IKEDLLFY.js";
|
|
229
13
|
|
|
230
14
|
// src/agent/model-call.ts
|
|
231
15
|
var CHAT_COMPLETIONS_PATH = "/v1/chat/completions";
|
|
@@ -659,9 +443,6 @@ function query(input) {
|
|
|
659
443
|
}
|
|
660
444
|
|
|
661
445
|
export {
|
|
662
|
-
httpPrimitiveTransport,
|
|
663
|
-
PRIMITIVE_BINDINGS,
|
|
664
|
-
createPrimitives,
|
|
665
446
|
callModelWithTools,
|
|
666
447
|
ryuTool,
|
|
667
448
|
resolveToolDefs,
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
// src/mcp/client.ts
|
|
2
|
+
import { spawn } from "child_process";
|
|
3
|
+
import { createInterface } from "readline";
|
|
4
|
+
var MCP_PROTOCOL_VERSION = "2024-11-05";
|
|
5
|
+
var RPC_TIMEOUT_MS = 6e4;
|
|
6
|
+
var McpConnection = class _McpConnection {
|
|
7
|
+
proc;
|
|
8
|
+
nextId = 1;
|
|
9
|
+
closed = false;
|
|
10
|
+
waiters = [];
|
|
11
|
+
constructor(proc) {
|
|
12
|
+
this.proc = proc;
|
|
13
|
+
}
|
|
14
|
+
/** Spawn the server and complete the MCP `initialize` handshake. */
|
|
15
|
+
static async connect(cmd) {
|
|
16
|
+
const env = { ...process.env, ...cmd.env ?? {} };
|
|
17
|
+
const proc = spawn(cmd.command, cmd.args ?? [], {
|
|
18
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
19
|
+
env
|
|
20
|
+
});
|
|
21
|
+
if (!(proc.stdin && proc.stdout)) {
|
|
22
|
+
proc.kill();
|
|
23
|
+
throw new Error(`MCP server '${cmd.command}' stdin/stdout unavailable`);
|
|
24
|
+
}
|
|
25
|
+
if (proc.stderr) {
|
|
26
|
+
proc.stderr.on("data", (chunk) => {
|
|
27
|
+
process.stderr.write(`[mcp-server] ${chunk.toString()}`);
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
const conn = new _McpConnection(proc);
|
|
31
|
+
const rl = createInterface({
|
|
32
|
+
input: proc.stdout,
|
|
33
|
+
crlfDelay: Number.POSITIVE_INFINITY
|
|
34
|
+
});
|
|
35
|
+
rl.on("line", (rawLine) => {
|
|
36
|
+
const trimmed = rawLine.trim();
|
|
37
|
+
if (!trimmed) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
let parsed;
|
|
41
|
+
try {
|
|
42
|
+
parsed = JSON.parse(trimmed);
|
|
43
|
+
} catch {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (parsed.id === void 0 || parsed.id === null) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const idx = conn.waiters.findIndex((w) => w.id === parsed.id);
|
|
50
|
+
if (idx === -1) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const [waiter] = conn.waiters.splice(idx, 1);
|
|
54
|
+
if (!waiter) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
clearTimeout(waiter.timer);
|
|
58
|
+
if (parsed.error) {
|
|
59
|
+
waiter.reject(new Error(`MCP error: ${JSON.stringify(parsed.error)}`));
|
|
60
|
+
} else {
|
|
61
|
+
waiter.resolve(parsed.result ?? null);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
rl.on("close", () => {
|
|
65
|
+
for (const waiter of conn.waiters.splice(0)) {
|
|
66
|
+
clearTimeout(waiter.timer);
|
|
67
|
+
waiter.reject(new Error("MCP server closed the connection"));
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
await conn.request("initialize", {
|
|
71
|
+
protocolVersion: MCP_PROTOCOL_VERSION,
|
|
72
|
+
capabilities: {},
|
|
73
|
+
clientInfo: { name: "ryu-sdk", version: "0.0.1" }
|
|
74
|
+
});
|
|
75
|
+
conn.notify("notifications/initialized", {});
|
|
76
|
+
return conn;
|
|
77
|
+
}
|
|
78
|
+
/** Send a JSON-RPC request and return the `result` field. */
|
|
79
|
+
request(method, params) {
|
|
80
|
+
const id = this.nextId++;
|
|
81
|
+
const frame = JSON.stringify({
|
|
82
|
+
jsonrpc: "2.0",
|
|
83
|
+
id,
|
|
84
|
+
method,
|
|
85
|
+
params
|
|
86
|
+
});
|
|
87
|
+
this.write(frame);
|
|
88
|
+
return new Promise((resolve, reject) => {
|
|
89
|
+
const timer = setTimeout(
|
|
90
|
+
() => reject(new Error(`MCP request '${method}' timed out`)),
|
|
91
|
+
RPC_TIMEOUT_MS
|
|
92
|
+
);
|
|
93
|
+
this.waiters.push({ id, resolve, reject, timer });
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
/** Send a JSON-RPC notification (no response expected). */
|
|
97
|
+
notify(method, params) {
|
|
98
|
+
const frame = JSON.stringify({ jsonrpc: "2.0", method, params });
|
|
99
|
+
this.write(frame);
|
|
100
|
+
}
|
|
101
|
+
write(frame) {
|
|
102
|
+
if (this.closed) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
this.proc.stdin?.write(`${frame}
|
|
106
|
+
`);
|
|
107
|
+
}
|
|
108
|
+
/** Graceful shutdown: close stdin, then kill. */
|
|
109
|
+
async shutdown() {
|
|
110
|
+
this.closed = true;
|
|
111
|
+
this.proc.stdin?.end();
|
|
112
|
+
await new Promise((resolve) => {
|
|
113
|
+
this.proc.once("exit", () => resolve());
|
|
114
|
+
this.proc.kill();
|
|
115
|
+
setTimeout(resolve, 500);
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
async function listTools(cmd) {
|
|
120
|
+
const conn = await McpConnection.connect(cmd);
|
|
121
|
+
let result;
|
|
122
|
+
try {
|
|
123
|
+
result = await conn.request("tools/list", {});
|
|
124
|
+
} finally {
|
|
125
|
+
await conn.shutdown();
|
|
126
|
+
}
|
|
127
|
+
const tools = result?.tools ?? [];
|
|
128
|
+
return tools.map((t) => {
|
|
129
|
+
const tool = t;
|
|
130
|
+
const name = tool.name;
|
|
131
|
+
if (typeof name !== "string") {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
return {
|
|
135
|
+
name,
|
|
136
|
+
description: typeof tool.description === "string" ? tool.description : void 0,
|
|
137
|
+
inputSchema: tool.inputSchema
|
|
138
|
+
};
|
|
139
|
+
}).filter((t) => t !== null);
|
|
140
|
+
}
|
|
141
|
+
async function callTool(cmd, tool, args) {
|
|
142
|
+
const conn = await McpConnection.connect(cmd);
|
|
143
|
+
try {
|
|
144
|
+
return await conn.request("tools/call", { name: tool, arguments: args });
|
|
145
|
+
} finally {
|
|
146
|
+
await conn.shutdown();
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export {
|
|
151
|
+
MCP_PROTOCOL_VERSION,
|
|
152
|
+
listTools,
|
|
153
|
+
callTool
|
|
154
|
+
};
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import {
|
|
2
|
+
PluginManifestSchema
|
|
3
|
+
} from "./chunk-IEUQ3CDG.js";
|
|
4
|
+
|
|
5
|
+
// src/runnable/app.ts
|
|
6
|
+
var DEFAULT_APP_WIDGET_MIME = "text/html+skybridge";
|
|
7
|
+
var DEFAULT_APP_DISPLAY_MODE = "inline";
|
|
8
|
+
var WIDGET_RENDER_GRANT = "widget:render";
|
|
9
|
+
function withWidgetRenderGrant(grants, widgets) {
|
|
10
|
+
const out = [...grants];
|
|
11
|
+
if (widgets.length > 0 && !out.includes(WIDGET_RENDER_GRANT)) {
|
|
12
|
+
out.push(WIDGET_RENDER_GRANT);
|
|
13
|
+
}
|
|
14
|
+
return out;
|
|
15
|
+
}
|
|
16
|
+
function appToolId(server, name) {
|
|
17
|
+
return `${server}.${name}`;
|
|
18
|
+
}
|
|
19
|
+
function defineApp(options) {
|
|
20
|
+
const server = options.server ?? options.slug;
|
|
21
|
+
const uri = `ui://widget/${options.slug}.html`;
|
|
22
|
+
const mime = options.mime ?? DEFAULT_APP_WIDGET_MIME;
|
|
23
|
+
const displayMode = options.displayMode ?? DEFAULT_APP_DISPLAY_MODE;
|
|
24
|
+
const hasCompanions = options.tools.some((t) => t.accessible === true);
|
|
25
|
+
const runnables = [];
|
|
26
|
+
const widgets = [];
|
|
27
|
+
for (const spec of options.tools) {
|
|
28
|
+
const isRender = spec.accessible !== true;
|
|
29
|
+
const id = appToolId(server, spec.name);
|
|
30
|
+
const config = {
|
|
31
|
+
slug: id,
|
|
32
|
+
description: spec.description,
|
|
33
|
+
widget: isRender,
|
|
34
|
+
widget_accessible: isRender ? hasCompanions : true,
|
|
35
|
+
...spec.inputSchema ? { input_schema: spec.inputSchema } : {},
|
|
36
|
+
...spec.invoking ? { invoking: spec.invoking } : {},
|
|
37
|
+
...spec.invoked ? { invoked: spec.invoked } : {}
|
|
38
|
+
};
|
|
39
|
+
runnables.push({
|
|
40
|
+
id,
|
|
41
|
+
name: spec.name,
|
|
42
|
+
kind: "tool",
|
|
43
|
+
config
|
|
44
|
+
});
|
|
45
|
+
if (isRender) {
|
|
46
|
+
widgets.push({
|
|
47
|
+
tool_id: id,
|
|
48
|
+
uri,
|
|
49
|
+
ui_entry: options.uiEntry,
|
|
50
|
+
mime,
|
|
51
|
+
default_display_mode: displayMode
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const contributes = {
|
|
56
|
+
turn_hooks: [],
|
|
57
|
+
chat_features: [],
|
|
58
|
+
// This builder synthesises an app from its runnables; an app that emits
|
|
59
|
+
// events declares them in a hand-authored `manifest.json`, same as
|
|
60
|
+
// `lsp_servers` below.
|
|
61
|
+
hook_events: [],
|
|
62
|
+
composer_controls: [],
|
|
63
|
+
settings_tabs: [],
|
|
64
|
+
slash_commands: [],
|
|
65
|
+
sidebar_sections: [],
|
|
66
|
+
sidebar_buttons: [],
|
|
67
|
+
dock_panels: [],
|
|
68
|
+
live_activities: [],
|
|
69
|
+
// Empty for the same reason as every sibling family above: this builder
|
|
70
|
+
// synthesises `widgets` from the app's own runnables and nothing else, and
|
|
71
|
+
// takes no `contributes` passthrough. An app that wants to declare language
|
|
72
|
+
// servers writes them in a hand-authored `manifest.json`.
|
|
73
|
+
lsp_servers: {},
|
|
74
|
+
// Same reason again: a danger-zone category, a Pi extension and an output
|
|
75
|
+
// style are all hand-authored declarations, not something derivable from
|
|
76
|
+
// runnables. An output style in particular points at a Markdown file next
|
|
77
|
+
// to the manifest, which this builder never writes.
|
|
78
|
+
data_categories: [],
|
|
79
|
+
pi_extensions: [],
|
|
80
|
+
output_styles: [],
|
|
81
|
+
message_actions: [],
|
|
82
|
+
widgets
|
|
83
|
+
};
|
|
84
|
+
const raw = {
|
|
85
|
+
id: options.id,
|
|
86
|
+
name: options.title,
|
|
87
|
+
version: options.version,
|
|
88
|
+
runnables,
|
|
89
|
+
// An app that synthesises widgets MUST hold `widget:render`, so this
|
|
90
|
+
// builder declares it rather than leaving the author to discover it.
|
|
91
|
+
//
|
|
92
|
+
// Core gates widget promotion on declared-AND-enabled-AND-granted, and a
|
|
93
|
+
// missing grant fails as `DeniedNoGrant` — which is an `info!` log and
|
|
94
|
+
// nothing else. The widget silently renders as plain text, with no error
|
|
95
|
+
// in the UI and nothing pointing at the manifest. Every app scaffolded
|
|
96
|
+
// through `defineApp` hit that, because the only fix was a grant string
|
|
97
|
+
// the templates never mention and the builder never added; the one
|
|
98
|
+
// working example on disk hand-writes it.
|
|
99
|
+
//
|
|
100
|
+
// Added only when there is a widget to render, and unioned rather than
|
|
101
|
+
// overwritten so an author's own `grants` list survives and re-declaring
|
|
102
|
+
// it is not an error.
|
|
103
|
+
permission_grants: withWidgetRenderGrant(options.grants ?? [], widgets),
|
|
104
|
+
activation_events: options.activationEvents ?? ["*"],
|
|
105
|
+
contributes,
|
|
106
|
+
// `targets: []` means EVERY surface, so an app that declares none is
|
|
107
|
+
// unrestricted — the backward-compatible default.
|
|
108
|
+
targets: options.targets ?? [],
|
|
109
|
+
// `requires` stays ABSENT (not `{apps:[],grants:[]}`) when undeclared, so the
|
|
110
|
+
// emitted manifest carries no key at all — matching Core's
|
|
111
|
+
// `Option<Requires>` + `skip_serializing_if = "Option::is_none"`.
|
|
112
|
+
...options.requires ? {
|
|
113
|
+
requires: {
|
|
114
|
+
apps: options.requires.apps ?? [],
|
|
115
|
+
capabilities: options.requires.capabilities ?? [],
|
|
116
|
+
grants: options.requires.grants ?? []
|
|
117
|
+
}
|
|
118
|
+
} : {}
|
|
119
|
+
};
|
|
120
|
+
const result = PluginManifestSchema.safeParse(raw);
|
|
121
|
+
if (!result.success) {
|
|
122
|
+
const first = result.error.issues[0];
|
|
123
|
+
const field = first?.path.join(".") ?? "unknown";
|
|
124
|
+
const message = first?.message ?? "validation failed";
|
|
125
|
+
throw new Error(
|
|
126
|
+
`manifest.json validation failed at '${field}': ${message}`
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
return result.data;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export {
|
|
133
|
+
appToolId,
|
|
134
|
+
defineApp
|
|
135
|
+
};
|