UNPKG

@modelcontextprotocol/server-everything

Version:

MCP server that exercises all the features of the MCP protocol

124 lines (123 loc) • 5.16 kB
// Resources a tool creates for a 2026-07-28 client to read later, served from // handles the server mints (#4852). // // On a 2025-era connection, `gzip-file-as-resource` registers the resource it // creates on the server instance serving the client's connection or session // (resources/session.ts), and the resource lasts as long as that instance. // The 2026-07-28 revision has no sessions, and on its HTTP path every request // is served by a fresh instance, so a resource registered on one would be gone // before the client could read it. Instead the resource is stored here, under a // URI that embeds a random id the server mints, and every 2026-07-28 instance // serves the store through one resource template. The id is the handle: a // client can read a minted resource only if it was given the URI. Each one // expires after ten minutes; an expired or unknown URI reads as not found // (-32602). import { randomUUID } from "node:crypto"; import { ResourceNotFoundError, ResourceTemplate, } from "@modelcontextprotocol/server"; import { RESOURCE_ICONS, RESOURCE_TEMPLATE_ICONS } from "../server/icons.js"; /** How long a minted resource stays readable, in milliseconds. */ export const MINTED_RESOURCE_TTL_MS = 10 * 60_000; /** * The most minted resources the process keeps, and the most bytes of content * across them. Each one may be as large as the gzip tool's fetch limit, so * without a budget a burst of calls could exhaust memory before the TTL frees * anything; past it, the oldest are dropped first. */ export const MAX_MINTED_RESOURCES = 100; export const MAX_MINTED_BYTES = 64 * 1024 * 1024; /** The URI template every minted resource matches. */ const mintedUriTemplate = "demo://resource/minted/{id}/{name}"; /** Every live minted resource, by URI, across every server instance. */ const minted = new Map(); /** Drop every minted resource past its expiry. */ function sweep(now) { for (const [uri, resource] of minted) { if (resource.expiresAt <= now) minted.delete(uri); } } /** * Drop the oldest minted resources (the map keeps insertion order) until one * more of `bytes` fits both budgets. */ function makeRoom(bytes) { let total = bytes; for (const [uri, resource] of minted) total += sizeOf(uri, resource); for (const [uri, resource] of minted) { if (minted.size < MAX_MINTED_RESOURCES && total <= MAX_MINTED_BYTES) break; minted.delete(uri); total -= sizeOf(uri, resource); } } /** What a stored resource counts against the budget: everything it keeps. */ function sizeOf(uri, resource) { return uri.length + resource.name.length + resource.blob.length; } /** * Store a blob resource under a freshly minted URI, and return a link to it. * Throws for a blob larger than `MAX_MINTED_BYTES`. * * @param name - The resource's name, the last segment of its URI. * @param mimeType - Its MIME type. * @param blob - Its content, base64-encoded. */ export function mintBlobResource(name, mimeType, blob) { // One resource larger than the whole budget is refused outright, rather // than evicting everything else and still exceeding the budget. const uri = `demo://resource/minted/${randomUUID()}/${encodeURIComponent(name)}`; const size = uri.length + name.length + blob.length; if (size > MAX_MINTED_BYTES) { throw new Error(`Cannot keep ${name} for later reading: its ${size} bytes exceed the ${MAX_MINTED_BYTES}-byte limit for minted resources`); } const now = Date.now(); sweep(now); makeRoom(size); minted.set(uri, { name, mimeType, blob, expiresAt: now + MINTED_RESOURCE_TTL_MS, }); return { type: "resource_link", uri, name, mimeType, description: `Created by a tool call; readable for ${MINTED_RESOURCE_TTL_MS / 60_000} minutes at this URI`, // #5434: a minted resource is a resource like any other (#3131). icons: RESOURCE_ICONS, }; } /** * Register the template that serves minted resources, on a 2026-07-28 * server. It lists nothing: a minted resource is reached by the URI a tool * returned. * * @param server - The 2026-07-28 server to serve them on. */ export function registerMintedResources(server) { server.registerResource("Minted Resource", new ResourceTemplate(mintedUriTemplate, { list: undefined }), { icons: RESOURCE_TEMPLATE_ICONS, description: "A resource a tool created for this client, such as gzip-file-as-resource's output. Readable for ten minutes at the URI the tool returned.", }, async (uri) => { sweep(Date.now()); const resource = minted.get(uri.toString()); if (!resource) throw new ResourceNotFoundError(uri.toString()); return { contents: [ { uri: uri.toString(), mimeType: resource.mimeType, blob: resource.blob, }, ], }; }); } /** Forget every minted resource. For tests. */ export function clearMintedResources() { minted.clear(); }