180 lines
9.4 KiB
JavaScript
180 lines
9.4 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import { createServer } from "node:http";
|
|
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import test from "node:test";
|
|
import { createFoundryAgentGateway } from "./foundry-agent-gateway.mjs";
|
|
import { createFoundryAgentStore } from "./foundry-agent-store.mjs";
|
|
import { handleFoundryMcpRequest } from "./foundry-mcp.mjs";
|
|
|
|
async function listen(server) {
|
|
await new Promise((resolve, reject) => {
|
|
server.once("error", reject);
|
|
server.listen(0, "127.0.0.1", resolve);
|
|
});
|
|
const address = server.address();
|
|
return `http://127.0.0.1:${address.port}`;
|
|
}
|
|
|
|
async function close(server) {
|
|
await new Promise((resolve) => server.close(resolve));
|
|
}
|
|
|
|
async function json(response) {
|
|
return await response.json();
|
|
}
|
|
|
|
test("one setup command provisions independent Foundry and Ontology MCP transports", async () => {
|
|
const dataRoot = await mkdtemp(path.join(os.tmpdir(), "nodedc-foundry-agent-gateway-"));
|
|
const installerPath = path.join(dataRoot, "installer.tgz");
|
|
await writeFile(installerPath, "installer", "utf8");
|
|
let ontologyAuthorization = "";
|
|
const ontologyServer = createServer(async (request, response) => {
|
|
ontologyAuthorization = String(request.headers.authorization || "");
|
|
const chunks = [];
|
|
for await (const chunk of request) chunks.push(chunk);
|
|
const message = JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
const result = message.method === "tools/list"
|
|
? { tools: [{ name: "ontology_status" }, { name: "ontology_search" }] }
|
|
: {
|
|
protocolVersion: "2025-06-18",
|
|
capabilities: { tools: { listChanged: false } },
|
|
serverInfo: { name: "nodedc-ontology-core", version: "test" },
|
|
};
|
|
response.writeHead(200, { "content-type": "application/json", "mcp-protocol-version": "2025-06-18" });
|
|
response.end(JSON.stringify({ jsonrpc: "2.0", id: message.id, result }));
|
|
});
|
|
const ontologyBase = await listen(ontologyServer);
|
|
const store = createFoundryAgentStore({ dataRoot: path.join(dataRoot, "store") });
|
|
const gateway = createFoundryAgentGateway({
|
|
store,
|
|
installerPackagePath: installerPath,
|
|
ontologyCoreUrl: ontologyBase,
|
|
ontologyCoreAccessToken: "platform-internal-token",
|
|
});
|
|
const operations = {
|
|
status: async () => ({ ok: true }),
|
|
listApplications: async () => ({ applications: [] }),
|
|
getApplication: async () => ({ application: null }),
|
|
};
|
|
const foundryServer = createServer(async (request, response) => {
|
|
const url = new URL(request.url || "/", `http://${request.headers.host}`);
|
|
if (await gateway.handlePublicRequest(request, response, url)) return;
|
|
if (await gateway.handleOntologyMcp(request, response, url)) return;
|
|
if (url.pathname === "/api/mcp") {
|
|
await handleFoundryMcpRequest(request, response, {
|
|
getConfig: () => ({ capabilitySecret: "", mcpAllowedOrigins: [] }),
|
|
operations,
|
|
authenticateAgentToken: gateway.authenticateFoundryToken,
|
|
});
|
|
return;
|
|
}
|
|
if (await gateway.handleManagementRequest(request, response, url, {
|
|
id: "user_root",
|
|
email: "dcctouch@gmail.com",
|
|
displayName: "DC SUDO",
|
|
})) return;
|
|
response.writeHead(404);
|
|
response.end();
|
|
});
|
|
const foundryBase = await listen(foundryServer);
|
|
try {
|
|
const createdResponse = await fetch(`${foundryBase}/api/foundry-agent-api/agents`, {
|
|
method: "POST",
|
|
headers: { "content-type": "application/json" },
|
|
body: JSON.stringify({ name: "Foundry Codex" }),
|
|
});
|
|
assert.equal(createdResponse.status, 201);
|
|
const created = await json(createdResponse);
|
|
|
|
const setupResponse = await fetch(`${foundryBase}/api/foundry-agent-api/agents/${created.agent.id}/setup-code`, { method: "POST" });
|
|
assert.equal(setupResponse.status, 201);
|
|
const setup = await json(setupResponse);
|
|
assert.match(setup.install.command, /nodedc-foundry-codex-agent/);
|
|
|
|
const code = setup.install.command.match(/--code\s+(fnd_setup_[A-Za-z0-9_-]+)/)?.[1];
|
|
assert.ok(code);
|
|
const redeemResponse = await fetch(`${foundryBase}/api/foundry-agent/v1/setup/redeem`, {
|
|
method: "POST",
|
|
headers: { "content-type": "application/json" },
|
|
body: JSON.stringify({ code, deviceName: "Test Codex" }),
|
|
});
|
|
assert.equal(redeemResponse.status, 200);
|
|
const redeemed = await json(redeemResponse);
|
|
assert.equal(redeemed.foundry.serverName, "nodedc_module_foundry");
|
|
assert.equal(redeemed.ontology.serverName, "nodedc_ontology");
|
|
assert.equal(redeemed.ontology.readOnly, true);
|
|
assert.notEqual(redeemed.foundry.token, redeemed.ontology.token);
|
|
|
|
const foundryListResponse = await fetch(redeemed.foundry.mcpUrl, {
|
|
method: "POST",
|
|
headers: { authorization: `Bearer ${redeemed.foundry.token}`, "content-type": "application/json" },
|
|
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/list", params: {} }),
|
|
});
|
|
assert.equal(foundryListResponse.status, 200);
|
|
const foundryList = await json(foundryListResponse);
|
|
assert.ok(foundryList.result.tools.some((tool) => tool.name === "foundry_create_application"));
|
|
const removePinTool = foundryList.result.tools.find((tool) => tool.name === "foundry_remove_map_pin_binding");
|
|
assert.deepEqual(removePinTool.inputSchema.required, ["applicationId", "pageId", "bindingId", "idempotencyKey"]);
|
|
assert.equal(removePinTool.inputSchema.additionalProperties, false);
|
|
const presentationTool = foundryList.result.tools.find((tool) => tool.name === "foundry_upsert_map_presentation_profile");
|
|
assert.equal(presentationTool.inputSchema.properties.profile.additionalProperties, false);
|
|
assert.ok(presentationTool.inputSchema.properties.profile.required.includes("facets"));
|
|
assert.ok(presentationTool.inputSchema.properties.profile.required.includes("target"));
|
|
assert.equal(Object.hasOwn(presentationTool.inputSchema.properties.profile.properties, "pin"), false);
|
|
assert.equal(presentationTool.inputSchema.properties.profile.properties.label.properties.sizePx.minimum, 8);
|
|
assert.equal(Object.hasOwn(presentationTool.inputSchema.properties.profile.properties.label.properties, "fontSizePx"), false);
|
|
assert.deepEqual(presentationTool.inputSchema.properties.profile.properties.label.properties.mode.enum, ["subject_id", "attributes", "none"]);
|
|
const mapSettingsTool = foundryList.result.tools.find((tool) => tool.name === "foundry_update_map_page_settings");
|
|
assert.ok(mapSettingsTool);
|
|
assert.equal(mapSettingsTool.inputSchema.properties.settings.additionalProperties, false);
|
|
assert.equal(Object.hasOwn(mapSettingsTool.inputSchema.properties.settings.properties, "providerUrl"), false);
|
|
const mapViewStateTool = foundryList.result.tools.find((tool) => tool.name === "foundry_save_map_page_view_state");
|
|
assert.ok(mapViewStateTool);
|
|
const subjectStateSchema = mapViewStateTool.inputSchema.properties.viewState.properties.subjectStates.items;
|
|
assert.deepEqual(subjectStateSchema.required, ["bindingId", "visible", "filters", "window"]);
|
|
assert.equal(subjectStateSchema.properties.filters.additionalProperties.items.type, "string");
|
|
assert.equal(subjectStateSchema.properties.window.properties.rect.additionalProperties, false);
|
|
const bindingTool = foundryList.result.tools.find((tool) => tool.name === "foundry_upsert_map_data_product_binding");
|
|
assert.equal(bindingTool.inputSchema.properties.binding.properties.displayName.maxLength, 120);
|
|
assert.equal(bindingTool.inputSchema.properties.binding.properties.order.maximum, 10000);
|
|
|
|
const crossTokenResponse = await fetch(redeemed.ontology.mcpUrl, {
|
|
method: "POST",
|
|
headers: { authorization: `Bearer ${redeemed.foundry.token}`, "content-type": "application/json" },
|
|
body: JSON.stringify({ jsonrpc: "2.0", id: 2, method: "tools/list", params: {} }),
|
|
});
|
|
assert.equal(crossTokenResponse.status, 401);
|
|
|
|
const ontologyListResponse = await fetch(redeemed.ontology.mcpUrl, {
|
|
method: "POST",
|
|
headers: { authorization: `Bearer ${redeemed.ontology.token}`, "content-type": "application/json" },
|
|
body: JSON.stringify({ jsonrpc: "2.0", id: 3, method: "tools/list", params: {} }),
|
|
});
|
|
assert.equal(ontologyListResponse.status, 200);
|
|
const ontologyList = await json(ontologyListResponse);
|
|
assert.deepEqual(ontologyList.result.tools.map((tool) => tool.name), ["ontology_status", "ontology_search"]);
|
|
assert.equal(ontologyAuthorization, "Bearer platform-internal-token");
|
|
|
|
const revokeResponse = await fetch(`${foundryBase}/api/foundry-agent-api/agents/${created.agent.id}/revoke`, { method: "POST" });
|
|
assert.equal(revokeResponse.status, 200);
|
|
const revokedFoundryResponse = await fetch(redeemed.foundry.mcpUrl, {
|
|
method: "POST",
|
|
headers: { authorization: `Bearer ${redeemed.foundry.token}`, "content-type": "application/json" },
|
|
body: JSON.stringify({ jsonrpc: "2.0", id: 4, method: "tools/list", params: {} }),
|
|
});
|
|
const revokedOntologyResponse = await fetch(redeemed.ontology.mcpUrl, {
|
|
method: "POST",
|
|
headers: { authorization: `Bearer ${redeemed.ontology.token}`, "content-type": "application/json" },
|
|
body: JSON.stringify({ jsonrpc: "2.0", id: 5, method: "tools/list", params: {} }),
|
|
});
|
|
assert.equal(revokedFoundryResponse.status, 401);
|
|
assert.equal(revokedOntologyResponse.status, 401);
|
|
} finally {
|
|
await close(foundryServer);
|
|
await close(ontologyServer);
|
|
await rm(dataRoot, { recursive: true, force: true });
|
|
}
|
|
});
|