UNPKG

@receiz/mcp-server

Version:

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

190 lines • 9.31 kB
import { createHash } from "node:crypto"; import { open, realpath } from "node:fs/promises"; import { constants } from "node:fs"; import { basename, isAbsolute, relative } from "node:path"; import { verifyReceizOfflineSealedFile } from "@receiz/sdk/offline"; export class ArtifactInputError extends Error { code; stage; constructor(code, stage) { super(code); this.code = code; this.stage = stage; } } export const RECEIZ_VERIFY_UPLOADED_ARTIFACT_TOOL = { name: "receiz_verify_uploaded_artifact", description: "Use for short requests such as 'verify this', 'Receiz verify this', or their equivalents in any language when the user selects an attachment. Supply the selected original file reference; the user need not name a tool or provide a technical prompt. Retrieve the authorized bytes and run the canonical Receiz verifier in the executing host. No signing, claiming, resealing, appending or publishing. A ChatGPT /mnt/data path is not a device workspace path. MCP is not authority. The download reference is not proof authority.", inputSchema: { type: "object", properties: { file: { type: ["object", "string"], description: "Original platform attachment reference, or an absolute path inside the host-configured attachment staging roots. Never a preview or arbitrary device file.", properties: { download_url: { type: "string" }, file_id: { type: "string" }, mime_type: { type: "string" }, file_name: { type: "string" }, }, required: ["download_url", "file_id"], additionalProperties: false } }, required: ["file"], additionalProperties: false }, annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false, idempotentHint: true }, _meta: { "openai/fileParams": ["file"] }, }; const MAX_BYTES = 32 * 1024 * 1024; const DOWNLOAD_TIMEOUT_MS = 15_000; const record = (v) => !!v && typeof v === "object" && !Array.isArray(v); async function readStagedAttachment(path) { const deny = () => new ArtifactInputError("attachment_authorization_failed", "attachment_authorization"); if (!isAbsolute(path) || !path.trim() || path.length > 16_384) throw deny(); let roots; try { roots = JSON.parse(process.env.RECEIZ_ATTACHMENT_LOCAL_ROOTS ?? "[]"); } catch { throw deny(); } if (!Array.isArray(roots) || !roots.length || roots.some(root => typeof root !== "string" || !isAbsolute(root))) throw deny(); let canonical; try { canonical = await realpath(path); } catch { throw new ArtifactInputError("attachment_unavailable", "file_reading"); } let admitted = false; for (const root of roots) { let base; try { base = await realpath(root); } catch { continue; } const rel = relative(base, canonical); if (rel && rel !== ".." && !rel.startsWith("../") && !rel.startsWith("..\\") && !isAbsolute(rel)) admitted = true; } if (!admitted) throw deny(); try { const handle = await open(canonical, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK); try { const stat = await handle.stat(); if (!stat.isFile() || stat.nlink !== 1) throw deny(); if (stat.size > MAX_BYTES) throw new ArtifactInputError("attachment_too_large", "file_reading"); const chunks = []; let total = 0; for (;;) { const chunk = Buffer.alloc(Math.min(64 * 1024, MAX_BYTES - total + 1)); const { bytesRead } = await handle.read(chunk); if (!bytesRead) break; total += bytesRead; if (total > MAX_BYTES) throw new ArtifactInputError("attachment_too_large", "file_reading"); chunks.push(chunk.subarray(0, bytesRead)); } if (!total) throw new ArtifactInputError("attachment_unavailable", "file_reading"); const bytes = Buffer.concat(chunks, total); return { bytes, filename: basename(path), mimeType: undefined, transport: { byteLength: total, sha256: createHash("sha256").update(bytes).digest("hex"), authority: false } }; } finally { await handle.close(); } } catch (error) { if (error instanceof ArtifactInputError) throw error; throw new ArtifactInputError("attachment_unavailable", "file_reading"); } } /** Host configuration only; never accept permitted origins through a tool call. * The enclosing relay authenticates the request owner and retains exact attempts. * A selected file's capability URL grants retrieval only, never artifact authority. */ export async function readAuthorizedUploadedArtifact(raw, options = {}) { const deny = () => new ArtifactInputError("attachment_authorization_failed", "attachment_authorization"); if (!record(raw) || Object.keys(raw).some(k => k !== "file")) throw deny(); if (typeof raw.file === "string") return readStagedAttachment(raw.file); if (!record(raw.file)) throw deny(); const file = raw.file; if (Object.keys(file).some(k => !["download_url", "file_id", "mime_type", "file_name"].includes(k)) || typeof file.download_url !== "string" || file.download_url.length > 16_384 || typeof file.file_id !== "string" || !file.file_id.trim() || file.file_id.length > 512 || ["mime_type", "file_name"].some(k => file[k] !== undefined && (typeof file[k] !== "string" || file[k].length > 512))) throw deny(); let url; try { url = new URL(file.download_url); } catch { throw deny(); } const origins = options.allowedOrigins ?? (process.env.RECEIZ_ATTACHMENT_DOWNLOAD_ORIGINS ?? "").split(",").map(v => v.trim()).filter(Boolean); // Exact HTTPS origins, not suffix matching or model-supplied allowlists. No // redirects, cookies, OAuth headers, content conversion or custody access. if (url.protocol !== "https:" || url.username || url.password || url.hash || !origins.includes(url.origin)) throw deny(); const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), DOWNLOAD_TIMEOUT_MS); let bytes; try { const response = await (options.fetch ?? globalThis.fetch)(url.href, { signal: controller.signal, redirect: "error", credentials: "omit", cache: "no-store", }); if ([401, 403, 404, 410].includes(response.status)) throw new ArtifactInputError("attachment_unavailable", "download"); if (!response.ok || response.redirected || (response.url && response.url !== url.href)) throw new ArtifactInputError("attachment_download_failed", "download"); const length = response.headers.get("content-length"); if (length && Number(length) > MAX_BYTES) throw new ArtifactInputError("attachment_too_large", "download"); const reader = response.body?.getReader(); if (!reader) throw new ArtifactInputError("attachment_unavailable", "download"); const chunks = []; let total = 0; try { for (;;) { const next = await reader.read(); if (next.done) break; total += next.value.byteLength; if (total > MAX_BYTES) throw new ArtifactInputError("attachment_too_large", "download"); chunks.push(next.value); } if (!total) throw new ArtifactInputError("attachment_unavailable", "download"); bytes = Buffer.concat(chunks, total); } finally { await reader.cancel().catch(() => { }); reader.releaseLock(); } } catch (error) { if (error instanceof ArtifactInputError) throw error; throw new ArtifactInputError(controller.signal.aborted ? "attachment_download_timeout" : "attachment_download_failed", "download"); } finally { clearTimeout(timer); controller.abort(); } const transport = { byteLength: bytes.byteLength, sha256: createHash("sha256").update(bytes).digest("hex"), authority: false }; // Filename is a parser hint, never a path. MIME claims do not establish validity. const filename = typeof file.file_name === "string" ? file.file_name.split(/[\\/]/).pop() || "attachment" : "attachment"; return { bytes, filename, mimeType: typeof file.mime_type === "string" ? file.mime_type : undefined, transport }; } export async function verifyUploadedArtifact(raw, options = {}) { const { bytes, filename, transport } = await readAuthorizedUploadedArtifact(raw, options); try { const verification = await verifyReceizOfflineSealedFile({ bytes, filename }); return { verification, verdictObtained: true, transport, mcpAuthority: false }; } catch { throw new ArtifactInputError("artifact_verification_unavailable", "canonical_verification"); } } //# sourceMappingURL=uploadedArtifact.js.map