@ryuhq/sdk 0.1.13 → 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
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import {
|
|
2
|
+
assertAllowedEgressUrl
|
|
3
|
+
} from "./chunk-IKEDLLFY.js";
|
|
4
|
+
|
|
5
|
+
// src/runnable/primitives.ts
|
|
6
|
+
function dataUrlToBytes(dataUrl) {
|
|
7
|
+
const match = /^data:([^;,]*)(;base64)?,([\s\S]*)$/.exec(dataUrl);
|
|
8
|
+
if (!match) {
|
|
9
|
+
throw new Error(
|
|
10
|
+
"stt.transcribe expects an `audio` value that is a data: URL (data:<mime>;base64,<data>)"
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
const mediaType = match[1] || "application/octet-stream";
|
|
14
|
+
const isBase64 = Boolean(match[2]);
|
|
15
|
+
const payload = match[3] ?? "";
|
|
16
|
+
if (isBase64) {
|
|
17
|
+
const binary = atob(payload);
|
|
18
|
+
const bytes = new Uint8Array(binary.length);
|
|
19
|
+
for (let i = 0; i < binary.length; i++) {
|
|
20
|
+
bytes[i] = binary.charCodeAt(i);
|
|
21
|
+
}
|
|
22
|
+
return { bytes, mediaType };
|
|
23
|
+
}
|
|
24
|
+
return {
|
|
25
|
+
bytes: new TextEncoder().encode(decodeURIComponent(payload)),
|
|
26
|
+
mediaType
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function bytesToDataUrl(bytes, mediaType) {
|
|
30
|
+
let binary = "";
|
|
31
|
+
const chunk = 32768;
|
|
32
|
+
for (let i = 0; i < bytes.length; i += chunk) {
|
|
33
|
+
binary += String.fromCharCode(...bytes.subarray(i, i + chunk));
|
|
34
|
+
}
|
|
35
|
+
return `data:${mediaType};base64,${btoa(binary)}`;
|
|
36
|
+
}
|
|
37
|
+
function httpPrimitiveTransport(options) {
|
|
38
|
+
const base = options.nodeUrl.replace(/\/+$/, "");
|
|
39
|
+
assertAllowedEgressUrl(base);
|
|
40
|
+
const doFetch = options.fetchImpl ?? fetch;
|
|
41
|
+
const authHeader = () => options.token ? { authorization: `Bearer ${options.token}` } : {};
|
|
42
|
+
const failDetail = async (path, res) => {
|
|
43
|
+
const detail = await res.text().catch(() => "");
|
|
44
|
+
return new Error(
|
|
45
|
+
`Ryu primitive call ${path} failed: ${res.status} ${res.statusText}${detail ? ` \u2014 ${detail}` : ""}`
|
|
46
|
+
);
|
|
47
|
+
};
|
|
48
|
+
const post = async (path, body) => {
|
|
49
|
+
const res = await doFetch(`${base}${path}`, {
|
|
50
|
+
method: "POST",
|
|
51
|
+
headers: { "content-type": "application/json", ...authHeader() },
|
|
52
|
+
body: JSON.stringify(body ?? {})
|
|
53
|
+
});
|
|
54
|
+
if (!res.ok) {
|
|
55
|
+
throw await failDetail(path, res);
|
|
56
|
+
}
|
|
57
|
+
const text = await res.text();
|
|
58
|
+
return text ? JSON.parse(text) : void 0;
|
|
59
|
+
};
|
|
60
|
+
const transcribeDirect = async (body) => {
|
|
61
|
+
const input = body ?? {};
|
|
62
|
+
if (!input.audio) {
|
|
63
|
+
throw new Error(
|
|
64
|
+
"stt.transcribe requires an `audio` data: URL (the recorded audio)"
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
const { bytes, mediaType } = dataUrlToBytes(input.audio);
|
|
68
|
+
const form = new FormData();
|
|
69
|
+
form.append(
|
|
70
|
+
"file",
|
|
71
|
+
new Blob([bytes], { type: mediaType || "audio/wav" }),
|
|
72
|
+
input.filename ?? "recording.wav"
|
|
73
|
+
);
|
|
74
|
+
const res = await doFetch(`${base}/api/voice/transcribe`, {
|
|
75
|
+
method: "POST",
|
|
76
|
+
headers: authHeader(),
|
|
77
|
+
body: form
|
|
78
|
+
});
|
|
79
|
+
if (!res.ok) {
|
|
80
|
+
throw await failDetail("/api/voice/transcribe", res);
|
|
81
|
+
}
|
|
82
|
+
const parsed = await res.json();
|
|
83
|
+
return (parsed.text ?? "").trim();
|
|
84
|
+
};
|
|
85
|
+
const speakDirect = async (body) => {
|
|
86
|
+
const res = await doFetch(`${base}/api/voice/speak`, {
|
|
87
|
+
method: "POST",
|
|
88
|
+
headers: { "content-type": "application/json", ...authHeader() },
|
|
89
|
+
body: JSON.stringify(body ?? {})
|
|
90
|
+
});
|
|
91
|
+
if (!res.ok) {
|
|
92
|
+
throw await failDetail("/api/voice/speak", res);
|
|
93
|
+
}
|
|
94
|
+
const bytes = new Uint8Array(await res.arrayBuffer());
|
|
95
|
+
const mediaType = res.headers.get("content-type") || "audio/wav";
|
|
96
|
+
return bytesToDataUrl(bytes, mediaType);
|
|
97
|
+
};
|
|
98
|
+
const generateImageDirect = async (body) => {
|
|
99
|
+
const res = await doFetch(`${base}/api/images/generate`, {
|
|
100
|
+
method: "POST",
|
|
101
|
+
headers: { "content-type": "application/json", ...authHeader() },
|
|
102
|
+
body: JSON.stringify(body ?? {})
|
|
103
|
+
});
|
|
104
|
+
if (!res.ok) {
|
|
105
|
+
throw await failDetail("/api/images/generate", res);
|
|
106
|
+
}
|
|
107
|
+
const parsed = await res.json();
|
|
108
|
+
return (parsed.data ?? []).map(
|
|
109
|
+
(item) => item.url ? item.url : `data:image/png;base64,${item.b64_json ?? ""}`
|
|
110
|
+
);
|
|
111
|
+
};
|
|
112
|
+
return {
|
|
113
|
+
bridge(method, args) {
|
|
114
|
+
if (!options.pluginId) {
|
|
115
|
+
return Promise.reject(
|
|
116
|
+
new Error(
|
|
117
|
+
`bridge primitive "${method}" requires a pluginId (the /api/plugins/:id/host caller identity)`
|
|
118
|
+
)
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
return post(`/api/plugins/${options.pluginId}/host`, { method, args });
|
|
122
|
+
},
|
|
123
|
+
direct(path, body) {
|
|
124
|
+
if (path === "/api/voice/transcribe") {
|
|
125
|
+
return transcribeDirect(body);
|
|
126
|
+
}
|
|
127
|
+
if (path === "/api/voice/speak") {
|
|
128
|
+
return speakDirect(body);
|
|
129
|
+
}
|
|
130
|
+
if (path === "/api/images/generate") {
|
|
131
|
+
return generateImageDirect(body);
|
|
132
|
+
}
|
|
133
|
+
return post(path, body);
|
|
134
|
+
},
|
|
135
|
+
capability(cap, body) {
|
|
136
|
+
return post(`/api/host/capability/${cap}`, body);
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
var PRIMITIVE_BINDINGS = {
|
|
141
|
+
// Bridge families (existing `PluginHookBridge`).
|
|
142
|
+
"engines.complete": {
|
|
143
|
+
transport: "bridge",
|
|
144
|
+
method: "model.complete",
|
|
145
|
+
grant: "hook:side-model"
|
|
146
|
+
},
|
|
147
|
+
"background.list": {
|
|
148
|
+
transport: "bridge",
|
|
149
|
+
method: "background.list",
|
|
150
|
+
grant: "background:control"
|
|
151
|
+
},
|
|
152
|
+
"background.stop": {
|
|
153
|
+
transport: "bridge",
|
|
154
|
+
method: "background.stop",
|
|
155
|
+
grant: "background:control"
|
|
156
|
+
},
|
|
157
|
+
// Host-direct media data-path (the host holds the node token; returns data: URLs).
|
|
158
|
+
"image.generate": {
|
|
159
|
+
transport: "direct",
|
|
160
|
+
path: "/api/images/generate",
|
|
161
|
+
grant: "media:generate"
|
|
162
|
+
},
|
|
163
|
+
"tts.speak": {
|
|
164
|
+
transport: "direct",
|
|
165
|
+
path: "/api/voice/speak",
|
|
166
|
+
grant: "media:generate"
|
|
167
|
+
},
|
|
168
|
+
"stt.transcribe": {
|
|
169
|
+
transport: "direct",
|
|
170
|
+
path: "/api/voice/transcribe",
|
|
171
|
+
grant: "media:transcribe"
|
|
172
|
+
},
|
|
173
|
+
// Broker capabilities — no rpc family yet (@requires-grant).
|
|
174
|
+
"rag.retrieve": { transport: "broker", capability: "rag" },
|
|
175
|
+
"rag.embed": { transport: "broker", capability: "rag" },
|
|
176
|
+
"rag.rerank": { transport: "broker", capability: "rag" },
|
|
177
|
+
"memory.recall": { transport: "broker", capability: "memory" },
|
|
178
|
+
"memory.store": { transport: "broker", capability: "memory" },
|
|
179
|
+
"realtime.broadcast": { transport: "broker", capability: "realtime" },
|
|
180
|
+
"realtime.subscribe": { transport: "broker", capability: "realtime" },
|
|
181
|
+
"durable.checkpoint": { transport: "broker", capability: "durable" },
|
|
182
|
+
"durable.resume": { transport: "broker", capability: "durable" },
|
|
183
|
+
"engines.embed": { transport: "broker", capability: "engines" }
|
|
184
|
+
};
|
|
185
|
+
function brokerCall(transport, cap, op, input) {
|
|
186
|
+
return transport.capability(cap, { op, input });
|
|
187
|
+
}
|
|
188
|
+
function createPrimitives(transport) {
|
|
189
|
+
return {
|
|
190
|
+
background: {
|
|
191
|
+
list: (input) => transport.bridge("background.list", {
|
|
192
|
+
producer: input?.producer,
|
|
193
|
+
running_only: input?.runningOnly
|
|
194
|
+
}),
|
|
195
|
+
stop: (input) => transport.bridge("background.stop", {
|
|
196
|
+
process_id: input.processId
|
|
197
|
+
})
|
|
198
|
+
},
|
|
199
|
+
rag: {
|
|
200
|
+
retrieve: (input) => brokerCall(transport, "rag", "retrieve", input),
|
|
201
|
+
embed: (input) => brokerCall(transport, "rag", "embed", input),
|
|
202
|
+
rerank: (input) => brokerCall(transport, "rag", "rerank", input)
|
|
203
|
+
},
|
|
204
|
+
memory: {
|
|
205
|
+
recall: (input) => brokerCall(transport, "memory", "recall", input),
|
|
206
|
+
store: (input) => brokerCall(transport, "memory", "store", input)
|
|
207
|
+
},
|
|
208
|
+
realtime: {
|
|
209
|
+
broadcast: (input) => brokerCall(transport, "realtime", "broadcast", input).then(
|
|
210
|
+
() => void 0
|
|
211
|
+
),
|
|
212
|
+
subscribe: (input) => brokerCall(
|
|
213
|
+
transport,
|
|
214
|
+
"realtime",
|
|
215
|
+
"subscribe",
|
|
216
|
+
input
|
|
217
|
+
)
|
|
218
|
+
},
|
|
219
|
+
durable: {
|
|
220
|
+
checkpoint: (input) => brokerCall(transport, "durable", "checkpoint", input),
|
|
221
|
+
resume: (input) => brokerCall(transport, "durable", "resume", input)
|
|
222
|
+
},
|
|
223
|
+
engines: {
|
|
224
|
+
// Bridge family: model.complete → host.sideModel (wire keys are snake_case).
|
|
225
|
+
complete: (input) => transport.bridge("model.complete", {
|
|
226
|
+
prompt: input.prompt,
|
|
227
|
+
system: input.system,
|
|
228
|
+
model: input.model,
|
|
229
|
+
model_pref_key: input.modelPrefKey,
|
|
230
|
+
effort: input.effort
|
|
231
|
+
}),
|
|
232
|
+
embed: (input) => brokerCall(transport, "engines", "embed", input)
|
|
233
|
+
},
|
|
234
|
+
tts: {
|
|
235
|
+
speak: (input) => transport.direct("/api/voice/speak", input)
|
|
236
|
+
},
|
|
237
|
+
stt: {
|
|
238
|
+
transcribe: (input) => transport.direct("/api/voice/transcribe", input)
|
|
239
|
+
},
|
|
240
|
+
image: {
|
|
241
|
+
generate: (input) => transport.direct("/api/images/generate", input)
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export {
|
|
247
|
+
httpPrimitiveTransport,
|
|
248
|
+
PRIMITIVE_BINDINGS,
|
|
249
|
+
createPrimitives
|
|
250
|
+
};
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import {
|
|
2
|
+
MCP_PROTOCOL_VERSION,
|
|
3
|
+
callTool,
|
|
4
|
+
listTools
|
|
5
|
+
} from "./chunk-VLIRNNAE.js";
|
|
6
|
+
|
|
7
|
+
// src/mcp/server.ts
|
|
8
|
+
import { createInterface } from "readline";
|
|
9
|
+
function respond(id, result) {
|
|
10
|
+
return { jsonrpc: "2.0", id: id ?? null, result };
|
|
11
|
+
}
|
|
12
|
+
function respondError(id, code, message) {
|
|
13
|
+
return { jsonrpc: "2.0", id: id ?? null, error: { code, message } };
|
|
14
|
+
}
|
|
15
|
+
function wrapContent(value) {
|
|
16
|
+
return {
|
|
17
|
+
content: [
|
|
18
|
+
{
|
|
19
|
+
type: "text",
|
|
20
|
+
text: typeof value === "string" ? value : JSON.stringify(value)
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
function unwrapContent(raw) {
|
|
26
|
+
const r = raw;
|
|
27
|
+
const text = r?.content?.[0]?.text;
|
|
28
|
+
if (text === void 0) {
|
|
29
|
+
return raw;
|
|
30
|
+
}
|
|
31
|
+
try {
|
|
32
|
+
return JSON.parse(text);
|
|
33
|
+
} catch {
|
|
34
|
+
return text;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
var McpServer = class {
|
|
38
|
+
runnables = /* @__PURE__ */ new Map();
|
|
39
|
+
passthroughs = [];
|
|
40
|
+
/**
|
|
41
|
+
* Register an SDK Runnable as an MCP tool.
|
|
42
|
+
* Returns `this` for chaining.
|
|
43
|
+
*/
|
|
44
|
+
register(runnable) {
|
|
45
|
+
this.runnables.set(runnable.name, runnable);
|
|
46
|
+
return this;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Register a passthrough to an external MCP stdio server (e.g. Ghost or
|
|
50
|
+
* Shadow). Tools from that server are fetched lazily and re-advertised.
|
|
51
|
+
* Returns `this` for chaining.
|
|
52
|
+
*/
|
|
53
|
+
passthrough(registration) {
|
|
54
|
+
this.passthroughs.push(registration);
|
|
55
|
+
return this;
|
|
56
|
+
}
|
|
57
|
+
/** Fetch all tools: local Runnables + passthrough tools. */
|
|
58
|
+
async allTools() {
|
|
59
|
+
const local = [...this.runnables.values()].map((r) => ({
|
|
60
|
+
name: r.name,
|
|
61
|
+
description: r.description,
|
|
62
|
+
inputSchema: r.inputSchema
|
|
63
|
+
}));
|
|
64
|
+
const remote = [];
|
|
65
|
+
for (const pt of this.passthroughs) {
|
|
66
|
+
try {
|
|
67
|
+
const tools = await listTools(pt.command);
|
|
68
|
+
remote.push(...tools);
|
|
69
|
+
} catch (err) {
|
|
70
|
+
process.stderr.write(
|
|
71
|
+
`[mcp-server] passthrough '${pt.label}' list_tools failed: ${err}
|
|
72
|
+
`
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return [...local, ...remote];
|
|
77
|
+
}
|
|
78
|
+
/** Handle a `tools/call` request. */
|
|
79
|
+
async handleCallTool(name, args) {
|
|
80
|
+
const local = this.runnables.get(name);
|
|
81
|
+
if (local) {
|
|
82
|
+
const result = await local.run(args);
|
|
83
|
+
return wrapContent(result);
|
|
84
|
+
}
|
|
85
|
+
for (const pt of this.passthroughs) {
|
|
86
|
+
try {
|
|
87
|
+
const tools = await listTools(pt.command);
|
|
88
|
+
if (tools.some((t) => t.name === name)) {
|
|
89
|
+
return await callTool(pt.command, name, args);
|
|
90
|
+
}
|
|
91
|
+
} catch {
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Handle a single parsed JSON-RPC request line. Returns the response to write
|
|
98
|
+
* (or null for notifications that require no response).
|
|
99
|
+
*/
|
|
100
|
+
async handleRequest(req, initialized, write) {
|
|
101
|
+
const { id, method, params } = req;
|
|
102
|
+
if (method === "initialize") {
|
|
103
|
+
initialized.value = true;
|
|
104
|
+
write(
|
|
105
|
+
respond(id, {
|
|
106
|
+
protocolVersion: MCP_PROTOCOL_VERSION,
|
|
107
|
+
capabilities: { tools: {} },
|
|
108
|
+
serverInfo: { name: "ryu-sdk-server", version: "0.0.1" }
|
|
109
|
+
})
|
|
110
|
+
);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (method === "notifications/initialized") {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (!initialized.value) {
|
|
117
|
+
write(respondError(id, -32002, "Server not initialized"));
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (method === "tools/list") {
|
|
121
|
+
try {
|
|
122
|
+
const tools = await this.allTools();
|
|
123
|
+
write(respond(id, { tools }));
|
|
124
|
+
} catch (err) {
|
|
125
|
+
write(respondError(id, -32603, String(err)));
|
|
126
|
+
}
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (method === "tools/call") {
|
|
130
|
+
await this.handleToolsCall(id, params, write);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
write(respondError(id, -32601, `Method not found: ${method}`));
|
|
134
|
+
}
|
|
135
|
+
/** Handle a `tools/call` JSON-RPC request. */
|
|
136
|
+
async handleToolsCall(id, params, write) {
|
|
137
|
+
const p = params;
|
|
138
|
+
const toolName = p?.name;
|
|
139
|
+
const toolArgs = p?.arguments ?? {};
|
|
140
|
+
if (typeof toolName !== "string") {
|
|
141
|
+
write(respondError(id, -32602, "tools/call requires 'name'"));
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
try {
|
|
145
|
+
const result = await this.handleCallTool(toolName, toolArgs);
|
|
146
|
+
write(respond(id, result));
|
|
147
|
+
} catch (err) {
|
|
148
|
+
write(
|
|
149
|
+
respond(id, {
|
|
150
|
+
content: [{ type: "text", text: String(err) }],
|
|
151
|
+
isError: true
|
|
152
|
+
})
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Start reading JSON-RPC requests from the given readable stream and writing
|
|
158
|
+
* responses to the given writable stream.
|
|
159
|
+
*
|
|
160
|
+
* Defaults to `process.stdin` / `process.stdout`. Passing explicit streams
|
|
161
|
+
* lets tests inject a pair of in-process pipes.
|
|
162
|
+
*
|
|
163
|
+
* Resolves when the input stream ends (EOF).
|
|
164
|
+
*/
|
|
165
|
+
serve(input = process.stdin, output = process.stdout) {
|
|
166
|
+
const write = (obj) => {
|
|
167
|
+
output.write(`${JSON.stringify(obj)}
|
|
168
|
+
`);
|
|
169
|
+
};
|
|
170
|
+
const initialized = { value: false };
|
|
171
|
+
return new Promise((resolve) => {
|
|
172
|
+
const rl = createInterface({
|
|
173
|
+
input,
|
|
174
|
+
crlfDelay: Number.POSITIVE_INFINITY
|
|
175
|
+
});
|
|
176
|
+
rl.on("line", (rawLine) => {
|
|
177
|
+
const line = rawLine.trim();
|
|
178
|
+
if (!line) {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
let req;
|
|
182
|
+
try {
|
|
183
|
+
req = JSON.parse(line);
|
|
184
|
+
} catch {
|
|
185
|
+
write(respondError(null, -32700, "Parse error"));
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
this.handleRequest(req, initialized, write).catch((err) => {
|
|
189
|
+
write(respondError(req.id, -32603, String(err)));
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
rl.on("close", resolve);
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
export {
|
|
198
|
+
unwrapContent,
|
|
199
|
+
McpServer
|
|
200
|
+
};
|
|
@@ -77,24 +77,7 @@ function defineModel(modelId, options = {}) {
|
|
|
77
77
|
return new ModelClient2(modelId, options);
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
// src/model/gateway.ts
|
|
81
|
-
import * as native2 from "@ryuhq/sdk-native";
|
|
82
|
-
var DEFAULT_GATEWAY_URL = "http://127.0.0.1:7981";
|
|
83
|
-
function resolveGatewayUrl3() {
|
|
84
|
-
return native2.resolveGatewayUrl();
|
|
85
|
-
}
|
|
86
|
-
function resolveGatewayToken2() {
|
|
87
|
-
return native2.resolveGatewayToken() ?? void 0;
|
|
88
|
-
}
|
|
89
|
-
function assertAllowedEgressUrl(baseUrl) {
|
|
90
|
-
native2.assertAllowedEgress(baseUrl);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
80
|
export {
|
|
94
81
|
ModelClient2 as ModelClient,
|
|
95
|
-
defineModel
|
|
96
|
-
DEFAULT_GATEWAY_URL,
|
|
97
|
-
resolveGatewayUrl3 as resolveGatewayUrl,
|
|
98
|
-
resolveGatewayToken2 as resolveGatewayToken,
|
|
99
|
-
assertAllowedEgressUrl
|
|
82
|
+
defineModel
|
|
100
83
|
};
|
|
@@ -146,8 +146,30 @@ var WidgetContributionSchema = z.object({
|
|
|
146
146
|
/** Default display mode (`inline` | `fullscreen` | `pip`). */
|
|
147
147
|
default_display_mode: z.string().default(DEFAULT_WIDGET_DISPLAY_MODE)
|
|
148
148
|
});
|
|
149
|
+
var ChatWidgetTemplateSchema = z.object({
|
|
150
|
+
id: z.string().regex(/^[a-z0-9][a-z0-9._:-]*$/),
|
|
151
|
+
title: z.string().min(1),
|
|
152
|
+
description: z.string().optional(),
|
|
153
|
+
triggers: z.array(z.string()).default([]),
|
|
154
|
+
examples: z.array(z.string()).default([]),
|
|
155
|
+
backing: z.object({
|
|
156
|
+
tool_id: z.string().regex(/^[A-Za-z0-9][A-Za-z0-9._:/-]*$/).optional(),
|
|
157
|
+
view_id: z.string().regex(/^[a-z0-9][a-z0-9._:-]*$/).optional()
|
|
158
|
+
}),
|
|
159
|
+
display_mode: z.string().min(1),
|
|
160
|
+
safe_action_ids: z.array(z.string().regex(/^[a-z0-9][a-z0-9._-]*$/)).default([]),
|
|
161
|
+
availability: z.string().default("available")
|
|
162
|
+
}).superRefine((value, ctx) => {
|
|
163
|
+
const count = Number(Boolean(value.backing.tool_id)) + Number(Boolean(value.backing.view_id));
|
|
164
|
+
if (count !== 1 && value.availability === "available") {
|
|
165
|
+
ctx.addIssue({ code: "custom", path: ["backing"], message: "available templates need exactly one backing tool_id or view_id" });
|
|
166
|
+
}
|
|
167
|
+
if (count > 1) {
|
|
168
|
+
ctx.addIssue({ code: "custom", path: ["backing"], message: "backing must declare at most one of tool_id or view_id" });
|
|
169
|
+
}
|
|
170
|
+
});
|
|
149
171
|
var ToolAppConfigSchema = z.object({
|
|
150
|
-
/** MCP tool slug this runnable wraps — the fully-qualified `<server
|
|
172
|
+
/** MCP tool slug this runnable wraps — the fully-qualified `<server>.<name>` id. */
|
|
151
173
|
slug: z.string().min(1),
|
|
152
174
|
/** The tool description the model reads when choosing it. Carried here because a
|
|
153
175
|
* packed app's manifest is the only channel (there is no `generated.rs`); Core's
|
|
@@ -174,6 +196,11 @@ var ContributesSchema = z.object({
|
|
|
174
196
|
* signing, leaving an app that emits events nothing is allowed to subscribe to. */
|
|
175
197
|
hook_events: z.array(HookEventContributionSchema).default([]),
|
|
176
198
|
composer_controls: z.array(z.record(z.string(), z.unknown())).default([]),
|
|
199
|
+
/** Chat feature descriptors whose behavior is implemented by the host shell.
|
|
200
|
+
* Mirrors the Rust-side `Contributes.chat_features`; keeping this field in the
|
|
201
|
+
* authoring schema prevents `ryu pack` from silently deleting a plugin's chat
|
|
202
|
+
* feature declaration before signing. */
|
|
203
|
+
chat_features: z.array(z.record(z.string(), z.unknown())).default([]),
|
|
177
204
|
settings_tabs: z.array(z.record(z.string(), z.unknown())).default([]),
|
|
178
205
|
slash_commands: z.array(z.record(z.string(), z.unknown())).default([]),
|
|
179
206
|
/** App widgets (Ryu Apps). Each binds a render tool id to its
|
|
@@ -181,6 +208,8 @@ var ContributesSchema = z.object({
|
|
|
181
208
|
* `Contributes.widgets` field, without which the CLI's zod parse would strip
|
|
182
209
|
* every widget an app authored here declares. */
|
|
183
210
|
widgets: z.array(WidgetContributionSchema).default([]),
|
|
211
|
+
/** Metadata-only chat widget templates. */
|
|
212
|
+
chat_widget_templates: z.array(ChatWidgetTemplateSchema).optional(),
|
|
184
213
|
/** App-registered sidebar sections (header + live list) and buttons (single nav
|
|
185
214
|
* rows). Loosely typed here — the shell owns the spec vocabulary — matching how
|
|
186
215
|
* `composer_controls`/`settings_tabs` are declared. Mirrors the Rust-side
|
|
@@ -193,6 +222,13 @@ var ContributesSchema = z.object({
|
|
|
193
222
|
* Rust-side `Contributes.dock_panels`; without it the CLI's zod parse would
|
|
194
223
|
* strip the dock panel an app declares here. */
|
|
195
224
|
dock_panels: z.array(z.record(z.string(), z.unknown())).default([]),
|
|
225
|
+
/** App-registered live activities (the desktop "Dynamic Island" cards). Loosely
|
|
226
|
+
* typed for the same reason as the surfaces above — the shell owns the
|
|
227
|
+
* `spec` vocabulary. Mirrors the Rust-side `Contributes.live_activities`;
|
|
228
|
+
* without it the CLI's zod parse would strip every live activity an app
|
|
229
|
+
* declares here, so a packed bundle would ship a dock that silently stays
|
|
230
|
+
* empty. */
|
|
231
|
+
live_activities: z.array(z.record(z.string(), z.unknown())).default([]),
|
|
196
232
|
/** Deletable data categories the app owns — one "Delete all X" row in Settings
|
|
197
233
|
* → Danger Zone. Mirrors the Rust-side `Contributes.data_categories`; without
|
|
198
234
|
* it the CLI's zod parse would strip the declaration before signing, and the
|
|
@@ -230,7 +266,10 @@ var ContributesSchema = z.object({
|
|
|
230
266
|
* bug: the styles' `.md` files are not carried by the bundle either, so there
|
|
231
267
|
* would be no residue to notice — the plugin would install clean and contribute
|
|
232
268
|
* nothing. */
|
|
233
|
-
output_styles: z.array(OutputStyleContributionSchema).default([])
|
|
269
|
+
output_styles: z.array(OutputStyleContributionSchema).default([]),
|
|
270
|
+
/** Per-message actions contributed by an enabled plugin. Kept as loose records
|
|
271
|
+
* so renderer-specific `kind`/`args` payloads survive `ryu pack` unchanged. */
|
|
272
|
+
message_actions: z.array(z.record(z.string(), z.unknown())).default([])
|
|
234
273
|
});
|
|
235
274
|
var SetupStepSchema = z.object({
|
|
236
275
|
/** Card heading (e.g. the companion app name). */
|
|
@@ -311,6 +350,61 @@ var EnginesReqSchema = z.object({
|
|
|
311
350
|
/** Floor for the Next.js web app. */
|
|
312
351
|
web: z.string().optional()
|
|
313
352
|
});
|
|
353
|
+
var McpServerAuthSchema = z.object({
|
|
354
|
+
client_id: z.string().min(1).optional(),
|
|
355
|
+
type: z.literal("oauth")
|
|
356
|
+
}).strict();
|
|
357
|
+
var McpServerDeclSchema = z.object({
|
|
358
|
+
args: z.array(z.string()).default([]),
|
|
359
|
+
auth: McpServerAuthSchema.optional(),
|
|
360
|
+
command: z.string().optional(),
|
|
361
|
+
command_env: z.string().optional(),
|
|
362
|
+
description: z.string().optional(),
|
|
363
|
+
enabled: z.boolean().default(true),
|
|
364
|
+
env: z.record(z.string(), z.string()).default({}),
|
|
365
|
+
headers: z.record(z.string(), z.string()).default({}),
|
|
366
|
+
type: z.enum(["stdio", "http", "streamable-http", "streamable_http", "sse"]).optional(),
|
|
367
|
+
url: z.url().optional()
|
|
368
|
+
}).superRefine((server, context) => {
|
|
369
|
+
if (!(server.command || server.url)) {
|
|
370
|
+
context.addIssue({
|
|
371
|
+
code: "custom",
|
|
372
|
+
message: "an MCP server requires command or url"
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
if (!server.auth) {
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
if (server.command || server.type === "stdio" || !server.url) {
|
|
379
|
+
context.addIssue({
|
|
380
|
+
code: "custom",
|
|
381
|
+
message: "OAuth is supported only for remote HTTP MCP servers"
|
|
382
|
+
});
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
if (Object.keys(server.headers).some(
|
|
386
|
+
(name) => name.toLowerCase() === "authorization"
|
|
387
|
+
)) {
|
|
388
|
+
context.addIssue({
|
|
389
|
+
code: "custom",
|
|
390
|
+
message: "OAuth cannot be combined with a static Authorization header"
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
const url = new URL(server.url);
|
|
394
|
+
const loopback = url.hostname === "localhost" || url.hostname.startsWith("127.") || url.hostname === "[::1]" || url.hostname === "::1";
|
|
395
|
+
if (url.username || url.password || url.hash) {
|
|
396
|
+
context.addIssue({
|
|
397
|
+
code: "custom",
|
|
398
|
+
message: "OAuth MCP URLs cannot contain credentials or fragments"
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
if (url.protocol !== "https:" && !(url.protocol === "http:" && loopback)) {
|
|
402
|
+
context.addIssue({
|
|
403
|
+
code: "custom",
|
|
404
|
+
message: "OAuth MCP URLs must use HTTPS except on loopback"
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
});
|
|
314
408
|
var PluginManifestSchema = z.object({
|
|
315
409
|
/** Reverse-domain unique identifier (e.g. `"com.example.my-plugin"`). */
|
|
316
410
|
id: z.string().min(1, "id is required"),
|
|
@@ -342,6 +436,8 @@ var PluginManifestSchema = z.object({
|
|
|
342
436
|
* Declarations only — grant enforcement is the Gateway's responsibility.
|
|
343
437
|
*/
|
|
344
438
|
permission_grants: z.array(z.string()).default([]),
|
|
439
|
+
/** Remote or stdio MCP servers registered by this plugin. */
|
|
440
|
+
mcp_servers: z.record(z.string(), McpServerDeclSchema).optional(),
|
|
345
441
|
/**
|
|
346
442
|
* Optional Companion surface (an in-desktop overlay or sidebar panel).
|
|
347
443
|
* Absent when the plugin has no Companion surface.
|
|
@@ -435,10 +531,16 @@ var PluginManifestSchema = z.object({
|
|
|
435
531
|
url: z.string().optional()
|
|
436
532
|
})
|
|
437
533
|
]).optional(),
|
|
534
|
+
/** Public source repository URL (Claude/Codex `repository`). */
|
|
535
|
+
repository: z.string().url().optional(),
|
|
536
|
+
/** True when the provider operates outside the local Ryu runtime. */
|
|
537
|
+
external: z.boolean().optional(),
|
|
438
538
|
/** Project/marketing homepage — maps to the listing `website` (Claude field). */
|
|
439
539
|
homepage: z.string().optional(),
|
|
440
540
|
/** Free-text search keywords (Claude field). */
|
|
441
541
|
keywords: z.array(z.string()).optional(),
|
|
542
|
+
/** Stable Marketplace filter labels (Ryu extension). */
|
|
543
|
+
tags: z.array(z.string()).optional(),
|
|
442
544
|
/** Taxonomy category beyond the runnable kinds (Claude field). */
|
|
443
545
|
category: z.string().optional(),
|
|
444
546
|
/** SPDX-ish license identifier (Claude field). */
|
|
@@ -452,6 +554,8 @@ var PluginManifestSchema = z.object({
|
|
|
452
554
|
* from `iconUrl` (a raster logo). Falls back to `iconUrl` when omitted.
|
|
453
555
|
*/
|
|
454
556
|
icon: z.string().optional(),
|
|
557
|
+
/** Optional detail-page hero banner metadata forwarded to the marketplace. */
|
|
558
|
+
banner: z.record(z.string(), z.unknown()).optional(),
|
|
455
559
|
/**
|
|
456
560
|
* Dithered-gradient background for the card's icon square (Ryu extension),
|
|
457
561
|
* mirroring dither-kit's `DitherGradient` props. `from`/`to` are a palette-colour
|
|
@@ -483,6 +587,22 @@ var PluginManifestSchema = z.object({
|
|
|
483
587
|
* array of steps guiding the user through post-install configuration.
|
|
484
588
|
*/
|
|
485
589
|
setup: z.union([SetupStepSchema, z.array(SetupStepSchema)]).optional()
|
|
590
|
+
}).superRefine((manifest, context) => {
|
|
591
|
+
const hasOAuthServer = Object.values(manifest.mcp_servers ?? {}).some(
|
|
592
|
+
(server) => server.auth?.type === "oauth"
|
|
593
|
+
);
|
|
594
|
+
if (!hasOAuthServer) {
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
for (const grant of ["mcp:server", "identity.read"]) {
|
|
598
|
+
if (!manifest.permission_grants.includes(grant)) {
|
|
599
|
+
context.addIssue({
|
|
600
|
+
code: "custom",
|
|
601
|
+
message: `OAuth MCP servers require the ${grant} permission grant`,
|
|
602
|
+
path: ["permission_grants"]
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
}
|
|
486
606
|
});
|
|
487
607
|
function validatePluginId(id) {
|
|
488
608
|
loadNative().validatePluginId(id);
|
|
@@ -504,6 +624,7 @@ export {
|
|
|
504
624
|
PiExtensionContributionSchema,
|
|
505
625
|
OutputStyleContributionSchema,
|
|
506
626
|
WidgetContributionSchema,
|
|
627
|
+
ChatWidgetTemplateSchema,
|
|
507
628
|
ToolAppConfigSchema,
|
|
508
629
|
ContributesSchema,
|
|
509
630
|
SetupStepSchema,
|
|
@@ -512,6 +633,8 @@ export {
|
|
|
512
633
|
RequiresSchema,
|
|
513
634
|
SurfaceSchema,
|
|
514
635
|
EnginesReqSchema,
|
|
636
|
+
McpServerAuthSchema,
|
|
637
|
+
McpServerDeclSchema,
|
|
515
638
|
PluginManifestSchema,
|
|
516
639
|
validatePluginId,
|
|
517
640
|
validateManifestStrict,
|