@relvo/mcp 0.0.0-stage → 0.1.1

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.
@@ -0,0 +1,242 @@
1
+ import { constants } from "node:fs";
2
+ import { link, lstat, mkdir, open, realpath, rename, unlink } from "node:fs/promises";
3
+ import { homedir } from "node:os";
4
+ import { dirname, isAbsolute, join, resolve } from "node:path";
5
+ import { createHash, randomUUID } from "node:crypto";
6
+ import { z } from "zod";
7
+ import { platformLocalSetupClientSchema, platformLocalSetupStartSchema, platformLocalSetupProofSchema, platformLocalSetupPollSchema } from "./local-setup.js";
8
+ import { configFromEnvironment, hasRelvoEnvironment } from "./index.js";
9
+ const registeredSchema = z.object({ client: z.enum(["claude", "codex"]), name: z.string().regex(/^(?:relvo|toolhub)-[a-f0-9]{24}$/), command: z.string().min(1), args: z.array(z.string()).min(1) }).strict();
10
+ const common = { managedBy: z.enum(["relvo-cli", "toolhub-cli"]), version: z.literal(1), apiUrl: z.string().url(), consoleUrl: z.string().url(), local: z.boolean(),
11
+ legacyProfileSha256: z.string().regex(/^[a-f0-9]{64}$/).optional(),
12
+ installationId: z.string().regex(/^[a-f0-9]{24}$/), registrations: z.array(registeredSchema).max(2).default([]) };
13
+ export const localProfileSchema = z.discriminatedUnion("state", [
14
+ z.object({ ...common, state: z.literal("pending"), client: platformLocalSetupClientSchema, request: platformLocalSetupStartSchema }).strict(),
15
+ z.object({ ...common, state: z.literal("ready"), grant: platformLocalSetupPollSchema.options[1], pendingAck: platformLocalSetupProofSchema.optional() }).strict(),
16
+ ]);
17
+ export const localProfilePath = (home = homedir()) => join(home, ".config", "relvo", "profile.json");
18
+ export const legacyLocalProfilePath = (home = homedir()) => join(home, ".config", "toolhub", "profile.json");
19
+ function owned(mode, uid, privateMode) {
20
+ if (typeof process.getuid === "function" && uid !== process.getuid() || mode & (privateMode ? 0o077 : 0o022))
21
+ throw new Error("Relvo local storage has unsafe ownership or permissions.");
22
+ }
23
+ export async function safeDirectory(path, create = false, privateMode = false) {
24
+ if (!isAbsolute(path) || resolve(path) !== path)
25
+ throw new Error("Relvo local paths must be canonical absolute paths.");
26
+ try {
27
+ const info = await lstat(path);
28
+ if (!info.isDirectory() || info.isSymbolicLink())
29
+ throw new Error("Relvo local directories must not be symlinks.");
30
+ owned(info.mode, info.uid, privateMode);
31
+ }
32
+ catch (error) {
33
+ if (!(error instanceof Error && "code" in error && error.code === "ENOENT") || !create)
34
+ throw error;
35
+ await mkdir(path, { mode: privateMode ? 0o700 : 0o755 });
36
+ await safeDirectory(path, false, privateMode);
37
+ }
38
+ }
39
+ export async function profileDirectory(path = localProfilePath(), create = false) {
40
+ if (process.platform === "win32")
41
+ throw new Error("Relvo private local profiles currently require POSIX file permissions.");
42
+ if (!isAbsolute(path) || resolve(path) !== path || !/\/\.config\/(?:relvo|toolhub)\/profile\.json$/.test(path))
43
+ throw new Error("Use the canonical managed Relvo profile path.");
44
+ const root = dirname(dirname(dirname(path)));
45
+ if (await realpath(root) !== root)
46
+ throw new Error("Relvo profile home must be a canonical directory.");
47
+ await safeDirectory(root);
48
+ await safeDirectory(join(root, ".config"), create);
49
+ await safeDirectory(dirname(path), create, true);
50
+ return dirname(path);
51
+ }
52
+ export async function readPrivateText(path, maximumBytes = 65536) {
53
+ const handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
54
+ try {
55
+ const info = await handle.stat();
56
+ owned(info.mode, info.uid, true);
57
+ if (!info.isFile() || info.size > maximumBytes || info.nlink !== 1)
58
+ throw new Error("Relvo private file is invalid.");
59
+ return await handle.readFile("utf8");
60
+ }
61
+ finally {
62
+ await handle.close();
63
+ }
64
+ }
65
+ export async function readPrivateJson(path, maximumBytes = 65536) { return JSON.parse(await readPrivateText(path, maximumBytes)); }
66
+ export async function writePrivateText(path, value, privateParent = true, maximumBytes = 65536) {
67
+ const directory = dirname(path);
68
+ await safeDirectory(directory, false, privateParent);
69
+ if (Buffer.byteLength(value, "utf8") > maximumBytes)
70
+ throw new Error("Relvo private file exceeds its size limit.");
71
+ try {
72
+ await readPrivateText(path, maximumBytes);
73
+ }
74
+ catch (error) {
75
+ if (!(error instanceof Error && "code" in error && error.code === "ENOENT"))
76
+ throw error;
77
+ }
78
+ const temporary = join(directory, `.relvo-${randomUUID()}.tmp`);
79
+ const handle = await open(temporary, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600);
80
+ let closed = false;
81
+ try {
82
+ await handle.writeFile(value);
83
+ await handle.sync();
84
+ await handle.close();
85
+ closed = true;
86
+ await rename(temporary, path);
87
+ const parent = await open(directory, constants.O_RDONLY);
88
+ try {
89
+ await parent.sync();
90
+ }
91
+ finally {
92
+ await parent.close();
93
+ }
94
+ }
95
+ catch (error) {
96
+ await unlink(temporary).catch(() => { });
97
+ throw error;
98
+ }
99
+ finally {
100
+ if (!closed)
101
+ await handle.close();
102
+ }
103
+ }
104
+ export async function writePrivateJson(path, value, privateParent = true, maximumBytes = 65536) { await writePrivateText(path, JSON.stringify(value) + "\n", privateParent, maximumBytes); }
105
+ export async function readLocalProfile(path = localProfilePath()) {
106
+ try {
107
+ path = await migratedProfileTarget(path);
108
+ let profile = await readOwnedProfile(path);
109
+ if (!profile && path.endsWith("/.config/relvo/profile.json")) {
110
+ const legacy = path.replace(/\/relvo\/profile\.json$/, "/toolhub/profile.json");
111
+ const previous = await readOwnedProfile(legacy);
112
+ if (previous) {
113
+ await assertLegacySetupIdle(legacy);
114
+ const raw = await readPrivateText(legacy);
115
+ const migrated = localProfileSchema.parse({ ...localProfileSchema.parse(JSON.parse(raw)), managedBy: "relvo-cli", legacyProfileSha256: digest(raw) });
116
+ await profileDirectory(path, true);
117
+ await createProfileExclusive(path, migrated);
118
+ profile = await readOwnedProfile(path);
119
+ }
120
+ }
121
+ if (!profile && path.endsWith("/.config/toolhub/profile.json"))
122
+ return readLocalProfile(path.replace(/\/toolhub\/profile\.json$/, "/relvo/profile.json"));
123
+ if (profile?.legacyProfileSha256)
124
+ await unchangedLegacyProfile(path, profile.legacyProfileSha256);
125
+ return profile;
126
+ }
127
+ catch (error) {
128
+ if (error instanceof Error && "code" in error && error.code === "ENOENT")
129
+ return null;
130
+ throw new Error("Relvo local profile is unsafe or invalid. No credentials were changed.");
131
+ }
132
+ }
133
+ const digest = (raw) => createHash("sha256").update(raw).digest("hex");
134
+ async function createProfileExclusive(path, profile) {
135
+ const value = JSON.stringify(profile) + "\n";
136
+ if (Buffer.byteLength(value, "utf8") > 65536)
137
+ throw new Error("The migrated profile exceeds its private file limit.");
138
+ const temporary = join(dirname(path), `.relvo-migrate-${randomUUID()}.tmp`);
139
+ const handle = await open(temporary, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600);
140
+ try {
141
+ try {
142
+ await handle.writeFile(value);
143
+ await handle.sync();
144
+ }
145
+ finally {
146
+ await handle.close();
147
+ }
148
+ try {
149
+ await link(temporary, path);
150
+ }
151
+ catch (error) {
152
+ if (!(error instanceof Error && "code" in error && error.code === "EEXIST"))
153
+ throw error;
154
+ }
155
+ }
156
+ finally {
157
+ await unlink(temporary);
158
+ }
159
+ const parent = await open(dirname(path), constants.O_RDONLY);
160
+ try {
161
+ await parent.sync();
162
+ }
163
+ finally {
164
+ await parent.close();
165
+ }
166
+ }
167
+ async function readOwnedProfile(path) {
168
+ try {
169
+ await profileDirectory(path);
170
+ return localProfileSchema.parse(await readPrivateJson(path));
171
+ }
172
+ catch (error) {
173
+ if (error instanceof Error && "code" in error && error.code === "ENOENT")
174
+ return null;
175
+ throw error;
176
+ }
177
+ }
178
+ async function migratedProfileTarget(path) {
179
+ if (!path.endsWith("/.config/toolhub/profile.json"))
180
+ return path;
181
+ const canonical = path.replace(/\/toolhub\/profile\.json$/, "/relvo/profile.json");
182
+ const profile = await readOwnedProfile(canonical);
183
+ return profile && (profile.legacyProfileSha256 || !await readOwnedProfile(path)) ? canonical : path;
184
+ }
185
+ async function assertLegacySetupIdle(path) {
186
+ let lock;
187
+ try {
188
+ lock = z.object({ managedBy: z.enum(["toolhub-cli", "relvo-cli"]), pid: z.number().int().positive() }).strict().parse(await readPrivateJson(join(dirname(path), "setup.lock")));
189
+ }
190
+ catch (error) {
191
+ if (error instanceof Error && "code" in error && error.code === "ENOENT")
192
+ return;
193
+ throw error;
194
+ }
195
+ try {
196
+ process.kill(lock.pid, 0);
197
+ }
198
+ catch (error) {
199
+ if (error instanceof Error && "code" in error && error.code === "ESRCH")
200
+ return;
201
+ throw error;
202
+ }
203
+ throw new Error("The existing setup is active. Wait for it to finish before migrating.");
204
+ }
205
+ async function unchangedLegacyProfile(path, expected) {
206
+ if (!path.endsWith("/.config/relvo/profile.json"))
207
+ throw new Error("Invalid migrated profile path");
208
+ const legacy = path.replace(/\/relvo\/profile\.json$/, "/toolhub/profile.json");
209
+ if (!await readOwnedProfile(legacy))
210
+ return null;
211
+ if (digest(await readPrivateText(legacy)) !== expected)
212
+ throw new Error("The existing profile changed after migration and was preserved.");
213
+ return legacy;
214
+ }
215
+ export async function writeLocalProfile(path, profile) {
216
+ path = await migratedProfileTarget(path);
217
+ await profileDirectory(path, true);
218
+ const previous = await readLocalProfile(path);
219
+ await writePrivateJson(path, localProfileSchema.parse({ ...profile, managedBy: "relvo-cli", ...(previous?.legacyProfileSha256 ? { legacyProfileSha256: previous.legacyProfileSha256 } : {}) }));
220
+ }
221
+ export async function removeLocalProfile(path) {
222
+ path = await migratedProfileTarget(path);
223
+ const profile = await readLocalProfile(path);
224
+ if (profile) {
225
+ const legacy = profile.legacyProfileSha256 ? await unchangedLegacyProfile(path, profile.legacyProfileSha256) : null;
226
+ if (legacy)
227
+ await unlink(legacy);
228
+ await unlink(path);
229
+ }
230
+ }
231
+ export async function runtimeConfig(env, source, profile, requireProfileMatch = false) {
232
+ const hasEnvironment = hasRelvoEnvironment(env) || ["NOIMOS_PLATFORM_API_URL", "NOIMOS_PLATFORM_API_KEY"].some((name) => env[name] !== undefined);
233
+ const environment = hasEnvironment || !profile ? configFromEnvironment(env, source) : null;
234
+ if (environment && (!profile || !requireProfileMatch))
235
+ return environment;
236
+ const saved = await readLocalProfile(profile);
237
+ if (!saved || saved.state !== "ready" || saved.pendingAck || Date.parse(saved.grant.expiresAt) <= Date.now())
238
+ throw new Error("Relvo login is missing, incomplete or expired. Run relvo setup to resume.");
239
+ if (environment && (environment.apiUrl.replace(/\/$/, "") !== saved.apiUrl || environment.apiKey !== saved.grant.apiKey || Boolean(environment.local) !== saved.local))
240
+ throw new Error("Ambient Relvo credentials conflict with the explicitly selected managed profile. No project was selected.");
241
+ return { apiUrl: saved.apiUrl, apiKey: saved.grant.apiKey, local: saved.local, source };
242
+ }
@@ -0,0 +1,92 @@
1
+ import { z } from "zod";
2
+ import { PLATFORM_CLIENT_HEADERS } from "../schema/platformApiKey.js";
3
+ import { platformLocalSetupStartInputSchema, platformLocalSetupStartSchema, platformLocalSetupProofSchema, platformLocalSetupPollSchema, platformLocalSetupAcknowledgedSchema } from "../schema/platformLocalSetup.js";
4
+ export { platformLocalSetupClientSchema, platformLocalSetupStartInputSchema, platformLocalSetupStartSchema, platformLocalSetupProofSchema, platformLocalSetupPollSchema, platformLocalSetupAcknowledgedSchema } from "../schema/platformLocalSetup.js";
5
+ export function relvoApiUrl(value, local = false) {
6
+ const url = new URL(value);
7
+ if (url.username || url.password || url.search || url.hash || (url.protocol !== "https:" && !(local && url.protocol === "http:" && ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname))))
8
+ throw new Error("Relvo URLs must use HTTPS; --local permits loopback HTTP only.");
9
+ return url.href.replace(/\/$/, "");
10
+ }
11
+ export const toolhubApiUrl = relvoApiUrl;
12
+ export function verificationUrl(value, consoleUrl, authorizationRequest, local = false) {
13
+ const trusted = new URL(relvoApiUrl(consoleUrl, local));
14
+ if (trusted.pathname !== "/")
15
+ throw new Error("Provide the Relvo console origin without a path.");
16
+ const url = new URL(value);
17
+ if (url.origin !== trusted.origin || url.pathname !== "/local-setup" || url.username || url.password || url.hash ||
18
+ [...url.searchParams.keys()].length !== 1 || url.searchParams.get("request") !== authorizationRequest)
19
+ throw new Error("Relvo returned an unexpected verification URL. No browser was opened.");
20
+ return url.href;
21
+ }
22
+ const publicSetupErrors = {
23
+ platform_local_setup_unavailable: "Local setup is unavailable on this Relvo server.",
24
+ platform_local_setup_invalid: "The local setup proof is invalid. No credentials were replaced.",
25
+ platform_local_setup_expired: "The local setup request expired. Run setup again after checking its saved state.",
26
+ platform_local_setup_conflict: "This setup request was already used for a different approval.",
27
+ platform_local_setup_rate_exceeded: "The local setup request limit was reached. Wait before trying again.",
28
+ };
29
+ export class LocalSetupClient {
30
+ transport;
31
+ baseUrl;
32
+ constructor(apiUrl, local = false, transport = fetch) {
33
+ this.transport = transport;
34
+ this.baseUrl = relvoApiUrl(apiUrl, local);
35
+ }
36
+ async request(operation, body, schema) {
37
+ let response;
38
+ try {
39
+ response = await this.transport(`${this.baseUrl}/tools/setup/${operation}`, { method: "POST", redirect: "error", signal: AbortSignal.timeout(20000),
40
+ headers: { "content-type": "application/json", [PLATFORM_CLIENT_HEADERS.relvo]: "cli" }, body: JSON.stringify(body) });
41
+ }
42
+ catch {
43
+ throw new Error("Relvo setup could not be reached. Rerun setup to resume its saved request; do not create another key manually.");
44
+ }
45
+ let payload;
46
+ try {
47
+ const reader = response.body?.getReader();
48
+ if (!reader)
49
+ throw new Error("Missing body");
50
+ const chunks = [];
51
+ let size = 0;
52
+ try {
53
+ for (;;) {
54
+ const { value, done } = await reader.read();
55
+ if (done)
56
+ break;
57
+ size += value.byteLength;
58
+ if (size > 32768) {
59
+ await reader.cancel();
60
+ throw new Error("Response too large");
61
+ }
62
+ chunks.push(value);
63
+ }
64
+ }
65
+ finally {
66
+ reader.releaseLock();
67
+ }
68
+ const bytes = new Uint8Array(size);
69
+ let offset = 0;
70
+ for (const chunk of chunks) {
71
+ bytes.set(chunk, offset);
72
+ offset += chunk.byteLength;
73
+ }
74
+ payload = JSON.parse(new TextDecoder().decode(bytes));
75
+ }
76
+ catch {
77
+ throw new Error("Relvo setup returned an invalid response. Rerun setup to resume the saved request.");
78
+ }
79
+ if (!response.ok) {
80
+ const error = z.object({ error: z.object({ message: z.string() }) }).safeParse(payload);
81
+ throw new Error(error.success && Object.hasOwn(publicSetupErrors, error.data.error.message) ? publicSetupErrors[error.data.error.message] : `Relvo setup request failed (${response.status}). No server details were printed.`);
82
+ }
83
+ const envelope = z.object({ data: z.unknown() }).safeParse(payload);
84
+ const result = schema.safeParse(envelope.success ? envelope.data.data : undefined);
85
+ if (!result.success)
86
+ throw new Error("Relvo setup returned an unexpected response. Rerun setup to resume the saved request.");
87
+ return result.data;
88
+ }
89
+ start(input) { return this.request("start", platformLocalSetupStartInputSchema.parse(input), platformLocalSetupStartSchema); }
90
+ poll(input) { return this.request("poll", platformLocalSetupProofSchema.parse(input), platformLocalSetupPollSchema); }
91
+ acknowledge(input) { return this.request("ack", platformLocalSetupProofSchema.parse(input), platformLocalSetupAcknowledgedSchema); }
92
+ }
@@ -0,0 +1,184 @@
1
+ import { z } from "zod";
2
+ import { platformJsonObjectSchema } from "../schema/platformPublicExecution.js";
3
+ const protocol = z.enum(["gemini-live", "openai-live", "openai-realtime"]);
4
+ export const realtimeVoiceConfigSchema = z.object({
5
+ model: z.string().min(1).max(100), priceVersion: z.string().min(1).max(40),
6
+ maximumChargeMicros: z.number().int().safe().min(50000).max(10000000).optional(),
7
+ maxDurationSeconds: z.number().int().min(1).max(300).optional(), maxOutputTokens: z.number().int().min(1).max(2048).optional(),
8
+ instructions: z.string().max(8000).optional(), locale: z.string().max(40).optional(), voice: z.string().max(80).optional(),
9
+ tools: z.array(z.object({ name: z.string(), description: z.string().optional(), parameters: platformJsonObjectSchema }).strict()).max(16).optional(),
10
+ clientOrigin: z.string().max(300).optional(),
11
+ }).strict();
12
+ export const realtimeVoiceModelsSchema = z.object({
13
+ priceVersion: z.string(), executionEnabled: z.boolean(),
14
+ models: z.array(z.object({ id: z.string(), provider: z.string(), protocol, inputSampleRate: z.number().int().positive(),
15
+ outputSampleRate: z.number().int().positive(), tools: z.enum(["functions", "delegation"]), configured: z.boolean() })),
16
+ });
17
+ export const realtimeVoiceGrantSchema = z.object({
18
+ clientSecret: z.string().min(1).max(2048), expiresAt: z.number().int(), path: z.literal("/v1/realtime"), protocol: z.literal("relvo-realtime"),
19
+ config: realtimeVoiceConfigSchema,
20
+ });
21
+ const startedSchema = z.object({ type: z.literal("relvo.session.started"), executionId: z.string().uuid(), model: z.string(), protocol,
22
+ inputSampleRate: z.number().int().positive(), outputSampleRate: z.number().int().positive() });
23
+ const closedSchema = z.object({ type: z.literal("relvo.session.closed"), executionId: z.string().uuid().nullable(), usageConfirmed: z.boolean(),
24
+ status: z.enum(["succeeded", "failed", "cancelled", "needs_review"]), providerCostMicros: z.number().int().nonnegative() });
25
+ export class RealtimeVoiceError extends Error {
26
+ executionId;
27
+ constructor(message, executionId) {
28
+ super(message);
29
+ this.executionId = executionId;
30
+ this.name = "RealtimeVoiceError";
31
+ }
32
+ }
33
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
34
+ export class RealtimeVoiceSession {
35
+ socket;
36
+ session;
37
+ requestClose;
38
+ closed;
39
+ constructor(socket, session, closed, requestClose) {
40
+ this.socket = socket;
41
+ this.session = session;
42
+ this.requestClose = requestClose;
43
+ this.closed = closed;
44
+ }
45
+ static connect(apiUrl, rawGrant, options = {}) {
46
+ const grant = realtimeVoiceGrantSchema.parse(rawGrant), url = new URL(grant.path, apiUrl), base = new URL(apiUrl);
47
+ if (base.username || base.password || base.search || base.hash || !["http:", "https:"].includes(base.protocol)
48
+ || (base.protocol === "http:" && !["127.0.0.1", "localhost", "[::1]"].includes(base.hostname)))
49
+ throw new Error("Use an HTTPS voice API URL or loopback HTTP.");
50
+ if (grant.expiresAt <= Date.now() / 1000)
51
+ throw new Error("Voice connection token expired. Request a new one.");
52
+ options.signal?.throwIfAborted();
53
+ url.protocol = base.protocol === "https:" ? "wss:" : "ws:";
54
+ const socket = (options.createWebSocket ?? ((target, protocols) => new WebSocket(target, protocols)))(url.href, [grant.protocol, `relvo-session.${grant.clientSecret}`]);
55
+ let complete = false, ready = false, closing = false, executionId = null, closeTimer, rejectReady = () => { };
56
+ let resolveClosed = () => { }, rejectClosed = () => { };
57
+ const closed = new Promise((resolve, reject) => { resolveClosed = resolve; rejectClosed = reject; });
58
+ void closed.catch(() => { });
59
+ const failure = (message) => { const error = new RealtimeVoiceError(message, executionId); rejectReady(error); rejectClosed(error); socket.close(); };
60
+ const cancelled = () => {
61
+ if (closing || complete)
62
+ return;
63
+ closing = true;
64
+ if (socket.readyState === 1)
65
+ socket.send(JSON.stringify({ type: "relvo.session.close" }));
66
+ else
67
+ socket.close();
68
+ closeTimer = setTimeout(() => failure("Voice finalization timed out. Read the execution receipt before retrying."), 25000);
69
+ };
70
+ options.signal?.addEventListener("abort", cancelled, { once: true });
71
+ socket.addEventListener("close", () => {
72
+ clearTimeout(timer);
73
+ clearTimeout(closeTimer);
74
+ options.signal?.removeEventListener("abort", cancelled);
75
+ if (!complete)
76
+ failure("Voice disconnected before final usage was confirmed. Read the execution receipt before retrying.");
77
+ }, { once: true });
78
+ socket.addEventListener("error", () => failure("The Relvo voice connection failed."), { once: true });
79
+ const timer = setTimeout(() => failure("Voice session setup timed out."), 25000);
80
+ return new Promise((resolve, reject) => {
81
+ rejectReady = reject;
82
+ socket.addEventListener("open", () => socket.send(JSON.stringify({ type: "relvo.session.configure", config: grant.config })), { once: true });
83
+ socket.addEventListener("message", event => {
84
+ let value;
85
+ try {
86
+ if (typeof event.data !== "string" || event.data.length > 1500000)
87
+ throw new Error("Invalid voice event");
88
+ value = JSON.parse(event.data);
89
+ }
90
+ catch {
91
+ failure("Invalid Relvo voice event.");
92
+ return;
93
+ }
94
+ if (!isRecord(value)) {
95
+ failure("Invalid Relvo voice event.");
96
+ return;
97
+ }
98
+ if (typeof value.executionId === "string" && z.string().uuid().safeParse(value.executionId).success)
99
+ executionId = value.executionId;
100
+ const started = startedSchema.safeParse(value), ended = closedSchema.safeParse(value);
101
+ if (started.success && !ready) {
102
+ ready = true;
103
+ clearTimeout(timer);
104
+ resolve(new RealtimeVoiceSession(socket, started.data, closed, cancelled));
105
+ }
106
+ if (ended.success) {
107
+ complete = true;
108
+ clearTimeout(closeTimer);
109
+ resolveClosed(ended.data);
110
+ if (!ready)
111
+ reject(new Error("Voice session ended during setup."));
112
+ socket.close();
113
+ }
114
+ if (value.type === "relvo.error" && !ready)
115
+ reject(new RealtimeVoiceError(`Voice session rejected: ${typeof value.code === "string" ? value.code : "unknown"}`, executionId));
116
+ try {
117
+ if (typeof value.delta === "string" && ["session.output_audio.delta", "response.output_audio.delta", "response.audio.delta"].includes(String(value.type)))
118
+ options.onAudio?.(value.delta, 24000);
119
+ if (isRecord(value.serverContent) && isRecord(value.serverContent.modelTurn) && Array.isArray(value.serverContent.modelTurn.parts)) {
120
+ for (const part of value.serverContent.modelTurn.parts)
121
+ if (isRecord(part) && isRecord(part.inlineData)
122
+ && typeof part.inlineData.data === "string" && typeof part.inlineData.mimeType === "string" && part.inlineData.mimeType.startsWith("audio/pcm"))
123
+ options.onAudio?.(part.inlineData.data, 24000);
124
+ }
125
+ options.onEvent?.(value);
126
+ }
127
+ catch {
128
+ failure("The voice event handler failed. Read the execution receipt before retrying.");
129
+ }
130
+ });
131
+ if (options.signal?.aborted)
132
+ cancelled();
133
+ });
134
+ }
135
+ send(event) {
136
+ if (this.socket.readyState !== 1 || this.socket.bufferedAmount > 1024 * 1024)
137
+ throw new Error("Voice is closed or its send buffer is full.");
138
+ const data = JSON.stringify(event);
139
+ if (new TextEncoder().encode(data).length > 65536)
140
+ throw new Error("Voice frame exceeds 64 KiB.");
141
+ this.socket.send(data);
142
+ }
143
+ sendAudio(audio) {
144
+ if (!audio || audio.length > 64000 || audio.length % 4 || !/^[A-Za-z0-9+/]+={0,2}$/.test(audio))
145
+ throw new Error("Provide a bounded base64 PCM16 frame.");
146
+ if (this.session.protocol === "gemini-live")
147
+ this.send({ realtimeInput: { audio: { data: audio, mimeType: `audio/pcm;rate=${this.session.inputSampleRate}` } } });
148
+ else
149
+ this.send({ type: this.session.protocol === "openai-live" ? "session.input_audio.append" : "input_audio_buffer.append", audio });
150
+ }
151
+ sendText(text) {
152
+ if (this.session.protocol === "gemini-live")
153
+ this.send({ clientContent: { turns: [{ role: "user", parts: [{ text }] }], turnComplete: true } });
154
+ else if (this.session.protocol === "openai-live")
155
+ this.sendDelegationResult(null, text);
156
+ else {
157
+ this.send({ type: "conversation.item.create", item: { type: "message", role: "user", content: [{ type: "input_text", text }] } });
158
+ this.send({ type: "response.create" });
159
+ }
160
+ }
161
+ sendToolResult(call) {
162
+ this.sendToolResults([call]);
163
+ }
164
+ sendToolResults(calls) {
165
+ if (!calls.length || calls.length > 16)
166
+ throw new Error("Return between one and sixteen tool results.");
167
+ if (this.session.protocol === "openai-live")
168
+ throw new Error("GPT-Live uses sendDelegationResult instead of function calls.");
169
+ if (this.session.protocol === "gemini-live")
170
+ this.send({ toolResponse: { functionResponses: calls.map(call => ({ id: call.id, name: call.name, response: call.output })) } });
171
+ else {
172
+ for (const call of calls)
173
+ this.send({ type: "conversation.item.create", item: { type: "function_call_output", call_id: call.id, output: JSON.stringify(call.output) } });
174
+ this.send({ type: "response.create" });
175
+ }
176
+ }
177
+ sendDelegationResult(delegationId, content) {
178
+ if (this.session.protocol !== "openai-live")
179
+ throw new Error("This model uses function calls instead of client delegation.");
180
+ this.send({ type: "session.commentary.append", delegation_id: delegationId, content });
181
+ }
182
+ close() { this.requestClose(); return this.closed; }
183
+ }
184
+ export const connectRealtimeVoice = RealtimeVoiceSession.connect;
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ import { startPlatformMcp } from "./runtime.js";
3
+ await startPlatformMcp();
@@ -0,0 +1,45 @@
1
+ import { lstat, realpath, stat } from "node:fs/promises";
2
+ import { basename, delimiter, isAbsolute, relative, resolve, sep } from "node:path";
3
+ import { uploadLocalInput } from "../client/local-input.js";
4
+ const sensitiveDirectories = new Set([
5
+ ".ssh", ".aws", ".gcloud", ".kube", ".config", ".git",
6
+ ".claude", ".agents", ".codex", ".cursor", "_sessions",
7
+ ]);
8
+ export async function uploadAuthorizedFile(client, filePath, configuredRoots) {
9
+ const roots = (configuredRoots ?? "").split(delimiter).map((root) => root.trim()).filter(Boolean);
10
+ if (!roots.length)
11
+ throw new Error("Local uploads are disabled. Set RELVO_MCP_ALLOWED_UPLOAD_ROOTS to the directories this MCP server may read.");
12
+ const allowed = await Promise.all(roots.map(async (root) => {
13
+ try {
14
+ if (!isAbsolute(root))
15
+ throw new Error("Relative root");
16
+ const canonical = await realpath(root);
17
+ if (!(await stat(canonical)).isDirectory())
18
+ throw new Error("Not a directory");
19
+ return canonical;
20
+ }
21
+ catch {
22
+ throw new Error("RELVO_MCP_ALLOWED_UPLOAD_ROOTS must contain existing absolute directories.");
23
+ }
24
+ }));
25
+ let canonical;
26
+ try {
27
+ const requested = resolve(filePath);
28
+ if (!(await lstat(requested)).isFile())
29
+ throw new Error("Not a regular file");
30
+ canonical = await realpath(requested);
31
+ }
32
+ catch {
33
+ throw new Error("Input must be an existing regular file. Symlinks are not accepted.");
34
+ }
35
+ if (!allowed.some((root) => {
36
+ const child = relative(root, canonical);
37
+ return child !== "" && !isAbsolute(child) && child !== ".." && !child.startsWith(`..${sep}`);
38
+ }))
39
+ throw new Error("File is outside RELVO_MCP_ALLOWED_UPLOAD_ROOTS.");
40
+ const name = basename(canonical).toLowerCase();
41
+ if (canonical.split(sep).some((part) => sensitiveDirectories.has(part.toLowerCase())) || name.startsWith(".env")) {
42
+ throw new Error("Refusing to upload a credential or secret path.");
43
+ }
44
+ return uploadLocalInput(client, canonical);
45
+ }
@@ -0,0 +1,28 @@
1
+ import { serveStdio } from "@modelcontextprotocol/server/stdio";
2
+ import { configFromEnvironment, PlatformClient } from "../client/index.js";
3
+ import { createPlatformMcp } from "./server.js";
4
+ import { runtimeConfig } from "../client/local-profile.js";
5
+ export async function startPlatformMcp(version = "0.1.1", profile) {
6
+ let handle;
7
+ try {
8
+ const client = new PlatformClient(profile ? await runtimeConfig(process.env, "mcp", profile, true) : configFromEnvironment(process.env, "mcp"));
9
+ handle = serveStdio(() => createPlatformMcp(client, version, true, Boolean(profile)), {
10
+ onerror: () => {
11
+ process.stderr.write(JSON.stringify({ severity: "ERROR", message: "Relvo MCP protocol error." }) + "\n");
12
+ },
13
+ });
14
+ const shutdown = async () => {
15
+ await handle?.close();
16
+ process.exit(0);
17
+ };
18
+ process.on("SIGINT", shutdown);
19
+ process.on("SIGTERM", shutdown);
20
+ }
21
+ catch {
22
+ process.stderr.write(JSON.stringify({
23
+ severity: "ERROR",
24
+ message: "Relvo MCP could not start. Check RELVO_API_URL and RELVO_API_KEY.",
25
+ }) + "\n");
26
+ process.exitCode = 1;
27
+ }
28
+ }