~ knotless Search/
↑↓ move↵ open⇧↵ all results
0
0
main
62 lines · 3.8 KB Raw History
// vendored from atlibs-asyncai 94afa33 modules/asyncai/mcp-stub.mjs
// Test MCP servers: mcpServer(tools) answers one JSON-RPC message (the in-process hook's contract);
// streamableHttp(server) serves it as a Streamable HTTP endpoint for ctx.http.fetch stubs.

/** tools: { name: (arguments) => CallToolResult }. Returns (message) => response, or null for a notification. */
export function mcpServer(tools, { version, pageSize = Infinity } = {}) {
	const names = Object.keys(tools);
	const ok = (id, result) => ({ jsonrpc: "2.0", id, result });
	const fail = (id, code, message) => ({ jsonrpc: "2.0", id, error: { code, message } });
	return (message) => {
		const { id, method, params = {} } = message;
		if (id === undefined) return null;
		if (method === "initialize") return ok(id, { protocolVersion: version ?? params.protocolVersion, capabilities: { tools: {} }, serverInfo: { name: "stub", version: "0" } });
		if (method === "ping") return ok(id, {});
		if (method === "tools/list") {
			const from = Number(params.cursor ?? 0);
			const page = names.slice(from, from + pageSize).map((name) => ({ name, description: name, inputSchema: { type: "object" } }));
			return ok(id, { tools: page, ...(from + pageSize < names.length && { nextCursor: String(from + pageSize) }) });
		}
		if (method === "tools/call") return tools[params.name] ? ok(id, tools[params.name](params.arguments ?? {})) : fail(id, -32602, `unknown tool ${params.name}`);
		return fail(id, -32601, `no method ${method}`);
	};
}

/** server as a Streamable HTTP endpoint: { handle(init) -> Response, seen: the requests }; sse answers as an event stream. */
export function streamableHttp(server, { sse = false, session } = {}) {
	const seen = [];
	const handle = async (init) => {
		const message = JSON.parse(init.body);
		const h = init.headers;
		seen.push({ method: message.method, params: message.params, session: h["mcp-session-id"], version: h["mcp-protocol-version"], accept: h.accept });
		const answer = server(message);
		if (message.id === undefined) return new Response(null, { status: 202 });
		const headers = { ...(message.method === "initialize" && session && { "mcp-session-id": session }) };
		if (!sse) return Response.json(answer, { headers });
		return new Response(`event: message\ndata: ${JSON.stringify(answer)}\n\n`, { headers: { ...headers, "content-type": "text/event-stream" } });
	};
	return { handle, seen };
}

/** handle as a host scheme fetcher (runModule's `fetchers`): { method, url, headers, body } -> { status, headers, body },
 * the headers cased as a Go server sends them (the guest agent's); `urls` gets each request's URL. */
export function fetcher(handle) {
	const urls = [];
	const fetch = async (request) => {
		urls.push(request.url);
		const response = await handle(request);
		const headers = Object.fromEntries([...response.headers].map(([k, v]) => [k.replace(/(^|-)[a-z]/g, (c) => c.toUpperCase()), v]));
		return { status: response.status, headers, body: await response.text() };
	};
	return { fetch, urls };
}

/** A reason to skip while this atlibs core routes no webrtc:// URL to a host fetcher (contract v2.2 section 3), else false. */
export async function noFetchers(runModule, ctx, mod) {
	const probe = { ...ctx, http: { fetch: async () => Response.json({ content: [], stop_reason: "end_turn" }) } };
	const input = { prompt: "-", endpoint: "https://inference.test", mcp: [{ name: "vm", url: "webrtc:///probe/shell/mcp" }] };
	const options = { http: { "https://inference.test": {}, "webrtc://*": {} }, fetchers: { "webrtc:": fetcher(streamableHttp(mcpServer({})).handle).fetch } };
	const ran = await runModule(probe, mod, "run", input, async () => {}, options).catch((e) => ({ error: e.message }));
	const server = ran.result?.servers?.[0];
	return server?.phase !== "ready" && `atlibs core routes no webrtc:// URL to a host fetcher yet (${server?.error ?? ran.error})`;
}