UNPKG

@receiz/mcp-server

Version:

MCP server for agents building and verifying Receiz proof-native applications through the canonical Receiz SDK rails.

236 lines • 13.4 kB
import { randomUUID, randomBytes } from "node:crypto"; import { mkdir, lstat, open, readdir, readFile, unlink, rename } from "node:fs/promises"; import { isAbsolute, join } from "node:path"; import { handleReceizMcpMessage } from "./index.js"; import { deviceRuntimeOwner, deviceRuntimeRelaySigner } from "./deviceOwner.js"; import { bindDeviceRelayWorkspace } from "./deviceWorkspace.js"; const uuid = /^[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/; /** Carry the relay's own attempt locator with an app-only proof chunk. The * locator lets the first-party Receiz route re-read the held exact bytes; it * never authorizes sealing, verification, or proof admission. */ export function attachReceizChunkAttempt(request, response, attemptId) { const params = request.params; if (request.method !== "tools/call" || !params || typeof params !== "object" || params.name !== "receiz_offline_artifact_chunk") return response; const result = response.result; if (!result || typeof result !== "object" || Array.isArray(result) || result.isError) return response; const toolResult = result; const publicPart = toolResult.structuredContent; if (!publicPart || typeof publicPart !== "object" || Array.isArray(publicPart)) return response; const meta = toolResult._meta; const privatePart = meta && typeof meta === "object" && !Array.isArray(meta) ? meta["receiz/sealedChunk"] : null; const privateBytes = privatePart && typeof privatePart === "object" && !Array.isArray(privatePart) ? privatePart.bytesBase64 : null; if (typeof publicPart.bytesBase64 !== "string" && typeof privateBytes !== "string") return response; toolResult.structuredContent = { ...publicPart, receizTransportAttemptId: attemptId }; return response; } /** Device-side transport. The owner is read from a genuine SDK admission, never * from a tool argument or OAuth claim. Completed replies retry; execution does not. */ export async function runReceizMcpDeviceRelay(options) { const { runtime, accessToken, directory, signal, onError } = options; const refresh = options.refresh ? Object.freeze({ ...options.refresh }) : undefined; const fetcher = options.fetch ?? globalThis.fetch; const endpoint = new URL(options.endpoint); if (endpoint.protocol !== "https:" || endpoint.username || endpoint.password || endpoint.search || endpoint.hash || !accessToken || !isAbsolute(directory)) throw Error("RECEIZ_DEVICE_RELAY_CONFIG_INVALID"); const owner = deviceRuntimeOwner(runtime); const transportOwner = options.transportOwnerId ?? owner; if (!transportOwner || transportOwner !== transportOwner.trim()) throw Error("RECEIZ_DEVICE_RELAY_CONFIG_INVALID"); const signRequest = deviceRuntimeRelaySigner(runtime); await mkdir(directory, { recursive: true, mode: 0o700 }); const stat = await lstat(directory); if (!stat.isDirectory() || (process.platform !== "win32" && (stat.mode & 0o077))) throw Error("RECEIZ_DEVICE_PRIVATE_CUSTODY_REQUIRED"); if (refresh && (!refresh.refreshToken || !refresh.clientId || refresh.clientId.length > 200)) throw Error("RECEIZ_DEVICE_RELAY_CONFIG_INVALID"); const resource = `${endpoint.origin}/api/mcp/device`; const tokenEndpoint = new URL("/api/oidc/token", endpoint.origin); const credentialsPath = join(directory, "oauth-transport.json"); let credentials = refresh ? { ownerId: owner, transportOwnerId: transportOwner, resource, clientId: refresh.clientId, accessToken, refreshToken: refresh.refreshToken } : undefined; const validToken = (value) => typeof value === "string" && value.length > 0 && value.length <= 32_000 && !/[\s\u0000-\u001f]/.test(value); if (refresh) { try { const file = await lstat(credentialsPath); if (!file.isFile() || file.size > 70_000 || (process.platform !== "win32" && (file.mode & 0o077))) throw Error("RECEIZ_DEVICE_PRIVATE_CUSTODY_REQUIRED"); const saved = JSON.parse(await readFile(credentialsPath, "utf8")); if (saved.ownerId !== owner || (saved.transportOwnerId ?? saved.ownerId) !== transportOwner || saved.resource !== resource || saved.clientId !== refresh.clientId) throw Error("RECEIZ_DEVICE_RELAY_IDENTITY_MISMATCH"); if (!validToken(saved.accessToken) || !validToken(saved.refreshToken)) throw Error("RECEIZ_DEVICE_RELAY_AUTH_STORAGE_FAILED"); credentials = saved; } catch (error) { if (error.code !== "ENOENT") { if (error instanceof Error && /^RECEIZ_DEVICE_[A-Z_]+$/.test(error.message)) throw error; throw Error("RECEIZ_DEVICE_RELAY_AUTH_STORAGE_FAILED"); } } } const readResponse = async (response) => { // Server responses are bounded independently of claimed Content-Length. const reader = response.body?.getReader(); if (!reader) throw Error("RECEIZ_DEVICE_RELAY_RESPONSE_INVALID"); const chunks = []; let count = 0; try { for (;;) { const item = await reader.read(); if (item.done) break; count += item.value.length; if (count > 1024 * 1024) { await reader.cancel(); throw Error("RECEIZ_DEVICE_RELAY_ENVELOPE_LIMIT"); } chunks.push(item.value); } } finally { reader.releaseLock(); } return JSON.parse(Buffer.concat(chunks).toString("utf8")); }; const refreshAccessToken = async () => { if (!refresh || !credentials) throw Error("RECEIZ_DEVICE_RELAY_AUTH_REQUIRED"); // Compose the existing resource-bound refresh grant. The endpoint, audience // and requested transport role are pinned by this host, never a challenge. const form = new URLSearchParams({ grant_type: "refresh_token", client_id: refresh.clientId, refresh_token: credentials.refreshToken, resource, scope: "openid offline_access mcp:device" }); if (refresh.clientSecret) form.set("client_secret", refresh.clientSecret); const response = await fetcher(tokenEndpoint, { method: "POST", headers: { "content-type": "application/x-www-form-urlencoded" }, body: form.toString(), signal: AbortSignal.any([signal, AbortSignal.timeout(15_000)]), redirect: "error" }); if (!response.ok) throw Error(response.status < 500 ? "RECEIZ_DEVICE_RELAY_AUTH_REQUIRED" : "RECEIZ_DEVICE_RELAY_TRANSPORT_UNAVAILABLE"); const result = await readResponse(response); const scopes = typeof result.scope === "string" ? result.scope.split(/\s+/).filter(Boolean) : []; if (result.token_type !== "Bearer" || !validToken(result.access_token) || !validToken(result.refresh_token) || typeof result.expires_in !== "number" || !Number.isFinite(result.expires_in) || result.expires_in <= 0 || !["openid", "offline_access", "mcp:device"].every(scope => scopes.includes(scope)) || scopes.some(scope => !["openid", "offline_access", "mcp:device"].includes(scope))) throw Error("RECEIZ_DEVICE_RELAY_AUTH_REQUIRED"); const next = { ownerId: owner, transportOwnerId: transportOwner, resource, clientId: refresh.clientId, accessToken: result.access_token, refreshToken: result.refresh_token }; const pending = join(directory, `.oauth-${randomUUID()}.pending`); try { const file = await open(pending, "wx", 0o600); try { await file.writeFile(JSON.stringify(next)); await file.sync(); } finally { await file.close(); } await rename(pending, credentialsPath); // Persist the rotated secret before sending another relay request. if (process.platform !== "win32") { const folder = await open(directory, "r"); try { await folder.sync(); } finally { await folder.close(); } } } catch { throw Error("RECEIZ_DEVICE_RELAY_AUTH_STORAGE_FAILED"); } finally { await unlink(pending).catch(() => undefined); } credentials = next; }; const post = async (body) => { const bodyText = JSON.stringify(body); const proofHeaders = await signRequest({ transportOwnerId: transportOwner, bodyText, actionId: randomBytes(32).toString("hex") }); const send = () => fetcher(endpoint, { method: "POST", headers: { ...proofHeaders, "content-type": "application/json", authorization: `Bearer ${credentials?.accessToken ?? accessToken}` }, body: bodyText, signal: AbortSignal.any([signal, AbortSignal.timeout(15_000)]), redirect: "error" }); let response = await send(); if (response.status === 401 && refresh) { await response.body?.cancel(); await refreshAccessToken(); response = await send(); } if (!response.ok) throw Error(response.status === 401 || response.status === 403 ? "RECEIZ_DEVICE_RELAY_AUTH_REQUIRED" : "RECEIZ_DEVICE_RELAY_TRANSPORT_UNAVAILABLE"); return readResponse(response); }; const flush = async () => { for (const name of await readdir(directory)) { if (!name.endsWith(".json") || !uuid.test(name.slice(0, -5))) continue; const path = join(directory, name); const file = await lstat(path); if (!file.isFile() || (process.platform !== "win32" && (file.mode & 0o077))) throw Error("RECEIZ_DEVICE_PRIVATE_CUSTODY_REQUIRED"); const reply = JSON.parse(await readFile(path, "utf8")); if (`${reply.attemptId}.json` !== name || !uuid.test(reply.workerAttempt)) throw Error("RECEIZ_DEVICE_RELAY_JOURNAL_INVALID"); if (reply.ownerId !== owner || (reply.transportOwnerId ?? reply.ownerId) !== transportOwner) throw Error("RECEIZ_DEVICE_RELAY_IDENTITY_MISMATCH"); const result = await post({ action: "complete", ...reply, transportOwnerId: transportOwner }); if (result.accepted !== true) throw Error("RECEIZ_DEVICE_RELAY_RESPONSE_INVALID"); await unlink(path); } }; while (!signal.aborted) { try { await flush(); const workerAttempt = randomUUID(); const reply = await post({ action: "claim", workerAttempt, ownerId: owner, transportOwnerId: transportOwner }); if (reply.ownerId !== owner || (reply.transportOwnerId ?? reply.ownerId) !== transportOwner) throw Error("RECEIZ_DEVICE_RELAY_IDENTITY_MISMATCH"); if (reply.job !== null) { const job = reply.job; if (!job || !uuid.test(job.attemptId) || job.ownerId !== transportOwner || job.expiresAt <= Date.now() || job.request?.id === undefined) throw Error("RECEIZ_DEVICE_RELAY_REQUEST_INVALID"); let result; try { const request = await bindDeviceRelayWorkspace(job.request, runtime.localCustody?.workspace ?? directory); result = attachReceizChunkAttempt(request, await handleReceizMcpMessage(request, runtime), job.attemptId); } catch { result = { jsonrpc: "2.0", id: job.request.id, error: { code: -32603, message: "Device execution failed; inspect held proof state before repeating this attempt." } }; } const pending = join(directory, `${job.attemptId}.pending`); const file = await open(pending, "wx", 0o600); try { await file.writeFile(JSON.stringify({ ownerId: owner, transportOwnerId: transportOwner, attemptId: job.attemptId, workerAttempt, response: result })); await file.sync(); } finally { await file.close(); } await rename(pending, join(directory, `${job.attemptId}.json`)); await flush(); } } catch (error) { if (signal.aborted) break; const code = error instanceof Error && /^RECEIZ_DEVICE_[A-Z_]+$/.test(error.message) ? error.message : "RECEIZ_DEVICE_RELAY_TRANSPORT_UNAVAILABLE"; onError?.(code); if (code === "RECEIZ_DEVICE_RELAY_AUTH_REQUIRED" || code === "RECEIZ_DEVICE_RELAY_IDENTITY_MISMATCH" || code === "RECEIZ_DEVICE_RELAY_AUTH_STORAGE_FAILED") throw Error(code); } if (!signal.aborted) await new Promise(resolve => { const done = () => { clearTimeout(timer); signal.removeEventListener("abort", done); resolve(); }; const timer = setTimeout(done, 1000); signal.addEventListener("abort", done, { once: true }); }); } } //# sourceMappingURL=deviceRelay.js.map